Dealing with Failure in Objective-C initializers

Most introductory texts on Objective-C cover the two step process to allocate and initialize a new object but do not say much about what you should do if the initialization fails. I suspect the reason for this is that most of the time initializers don’t do much that could go wrong. One situation where an initializer can fail is when it depends on a resource which may not always be available or which could be in error. For example, data which must be retrieved from a file or a remote network resource.

Initializing Objects

Before looking at how to handle failure conditions in the initializer it is worth reviewing some of the key points about an Objective-C initializer. A simple but fairly typical initializer for an Objectve-C class may look something like this:

- (id)initWithValue:(NSUInteger)someValue {

  self = [super init];
  if (nil != self) {

    // Set some ivars
    someIvar = someValue;
  }

  return self;
}

The key points to note are as follows:

  • By convention the name of the initializer always starts with “init”
  • The return type should always be “id”
  • The initializer should always call the designated initialiser of its superclass. If the class is a direct subclass of NSObject (e.g. @Interface MyClass : NSObject) the designated initializer is “init”. If you are subclassing some other class you should check the class documentation or interface file to determine the designated initializer.
  • The value returned by the designated initializer should be assigned to self
  • If and only if the call to the initializer of the superclass did not return nil you can access the instance variables and set the values you need the new object to contain
  • Setting the ivars is done directly rather than via self (e.g. self.someIvar=…) to avoid invoking any getter/setter methods.
  • When an object is allocated all of the instance variables are set to 0 (or nil in the case of object pointers) so you do not need to zero ivars in the initializer.
  • The initializer should return self if successful otherwise it should return nil.

Dealing with Failure

In this simple example of an Objective-C initializer handling failure is pretty simple. The only way the initializer can really fail is if the initializer of the superclass fails. In that case there is nothing to do but return nil to the caller (making sure not to access any of the ivars). Testing for a failure at this point is good practise even if you think the initializer can never fail since you never know how the superclass may be modified in the future.

So what happens when our initializer is a bit more complicated and needs to access some external resources to properly initialize the object:

- (id)initWithFile:(NSString *)filePath {

  self = [super init];
  if (nil != self) {
    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
      // More code to read, validate and use file contents to
      // init the object
    } else {
      [self release];
      return nil;
    }
  }

  return self;
}

In this case we have an initializer that takes a single parameter which should be the path to a file used to initialize the object. After first initializing the superclass and checking for failure as before the class checks that the file actually exists. If the file does not exist the object cannot be initialized so we need to clean up and return nil to the caller.

However in this case we have already successfully initialized the superclass so we have a partially initialized object. We do not want to return this object to the caller since it is not fully initialized but if we just return nil we will create a memory leak. The solution is to send the release message to the superclass and let it clean up the resources allocated by the superclass.

In practise this type of initializer would be more complicated with many ways for failure. For example, we may start reading the file and only then find it is invalid. However the basic approach to failure is always the same. Any resources allocated in the initializer need to be released then the release message needs to be sent to the superclass. Finally we should return nil to the caller.