Many developers have been distraught over Swift's lack of robust error handling to date—queue in Swift 2.0! With the new Swift 2.0 release, Apple brings their own flavor of the traditional try/catch/finally exception handling found in other programming languages. This post will give you a little bit of background and some implementation examples using Swift's new error handling features.

Before Swift 2.0

The use of NSError in methods or functions that end in failure is currently the most popular way of handling errors for both Swift and Objective-C developers. With the use of NSError, methods or functions may return a Boolean to indicate failure and then use the NSErrorPointer as a parameter to provide detailed information about the failure.

However, the problem with the use of NSError is that it does not enforce error checking—you can pass nil to completely ignore the error or you can pass NSError to a method but never use it.

Native exception handling is also available in Objective-C, however the use of it never became widespread as it often leads to uncaught exceptions and restricts the developer to handling unrecoverable errors.

NSString * test = @"testString";
unichar a;
@try {
 a = [test characterAtIndex:10];
}
@catch (NSException * exception) {
 NSLog(@"%@", exception.reason);
}
@finally {
 //execute here whether exception was found or not
}

What's New with Swift 2.0

Try, Catch, Do, and Throw

In a nutshell—the throws statement is used when declaring a method that can throw an error, then do, try, and catch statements are implemented when you call on that method. Here is the breakdown:

  1. Any error you wish to throw must conform to the ErrorType protocol. (NSError conforms to this). For example:
    enum MyError: ErrorType { 
     case ErrorOne 
     case ErrorTwo (reason: String) 
    }

    Note: You may parameterize your errors.

  2. The function is marked with a throws declaration and may throw any or all of the declared errors:
    func functionWithAnError (testString: String) throws 
     if testString == "testString" { 
     throw MyError.ErrorOne 
     } else if testString == "apples" { 
     throw MError.ErrorTwo (reason: "\(testString) is not allowed.") 
     } 
    }
  3. Catch the errors using the new do-try-catch statement:
    func testErrorHandling () { 
     do { 
     try functionWithAnError("apples") 
     } catch MyError.ErrorOne { 
     print("Error one occurred.") 
     } catch MyError.ErrorTwo (let reason) { 
     //catch parameterized error print("Error two ocurred with reason: \(reason)") 
     } catch let unknownError {
     print("\(uknownError) occurred.") 
     } 
    }

    Note: Even though we know that we caught all possible error types, the compiler does not. Therefore to satisfy the compiler, we must add a catch all scenario, similar to a default catch in a switch statement. See "Usability Improvements" below for more detail.

What if I don't want to catch an error?

You can also disable any method that declares a throws by adding a try! before calling that method in your code. This means that you are making a promise that the function will not return an error—if it does, execution will halt. For example:

func dontTestErrorHandling () {
 //no problem
 try! functionWithAnError("no apples")
 //function hits ErrorTwo and application crashes
 try! functionWithAnError("apples")
}

Where is the finally?

In Swift's rendition of the traditional try-catch-finally, Apple adds another new clause—the defer statement. You might be wondering what the difference between defer and finally is—they are ultimately the same. They key difference with Swift's new defer statement is timing. The defer statement implies that you want some statement or work to be performed, but not yet, whereas with the finally statement you are simply implying that you want that work done regardless of what happens during execution. There is no timing involved in the finally statement.

func doSomething () throws {
 print("first")
 defer {
 print("last")
 } 
 defer {
 print("third")
 }

 throw MyError.ErrorOne
 print("second")
}

In this code, the following things will happen:

  1. "first" will be printed
  2. ErrorOne will be thrown
  3. "second" will never be printed as the method will exit
  4. "third" will be printed
  5. "last" will be printed

It is worth noting that when declaring multiple defer statements, they will always get called in reverse order of declaration and will always be called at the end of execution no matter where they are declared within a method.

The Guard Statement

Often times when using Optionals in swift, we only want to perform an action if the Optional does not contain a value. This is where the guard statement comes in handy. The significant difference between using an if-let statement and a guard (let-else) is that guard unwraps the optional outside of its scope and it runs only if conditions are not met.

let string : String?
guard let thisString = string else {
 //perform actions here!
}
print("Use \(thisString) here if you have it.")

In this example, the "else" statement is only executed if the value does not exist. If the value does exist, it is unwrapped and the else statement is not executed. Notice that unlike and if-let statement, the unwrapped variable is accessible outside of the block if it contains a value. This makes checking for nil values much easier and makes your code much more readable! It also plays a great role in error handling as it works very similarly to the assertion statement, but does not result in a fatal crash.

Technical Improvements

When an exception is thrown in C or Objective-C but not caught within the current scope, the call stack is immediately unwound which often leads to memory leaks due to skipped release statements for pointers. Garbage collected Objective-c code does mitigate this problem, however throwing an exception within any C code that is not wrapped in an Objective-C object is subject to memory leak. This is why it has never been advised to throw exceptions in Objective-C. Below is an example of how a memory leak can occur after an exception is thrown.

Call stack:

 foo3() **Exception thrown here
 foo2()
 foo1()
 main() **Exception caught here

Arbitrary C implementation of the foo2 method:

<span class="kw4">void</span> foo2 <span class="br0">(</span><span class="br0">)</span> <span class="br0">{</span>
 Dummy <span class="sy0">*</span> ptr <span class="sy0">=</span> new Dummy<span class="br0">(</span><span class="br0">)</span><span class="sy0">;</span>
 foo3<span class="br0">(</span><span class="br0">)</span><span class="sy0">;</span> <span class="co2"># Stack immediately unwinds when exception is thrown in this method</span>
 <a href="http://www.opengroup.org/onlinepubs/009695399/functions/free.html"><span class="kw3">free</span></a><span class="br0">(</span>ptr<span class="br0">)</span><span class="sy0">;</span> <span class="co2"># Never reached--memory leak!</span>
<span class="br0">}</span>

As the call stack unwinds searching for the exception thrown in foo3() to be caught, it works its way down through each method until it reaches main() where the exception is caught (or the bottom of the stack in the case of an uncaught exception). In this example, you can see that the pointer to the Dummy object allocated in foo2 will never get released and a memory leak will occur.

In contrast, the do-catch block structure of Swift 2.0's new error handling causes execution to immediately exit the scope to handle thrown exceptions rather than unwinding through the call stack.

Returning to a snippet of the swift example I used earlier:

func testErrorHandling () {
 do {
 try functionWithAnError("apples") 
 } catch MyError.ErrorOne {
 print("Error one occurred.")
 }
}

When the exception is thrown in functionWithAnError the testErrorHandling function returns, breaks out of the do block and catches the exception—no risk of memory leak.

Usability Improvements

In Objective-C catching all possible exceptions that could be thrown by a function was optional, however in Swift 2.0 this is now mandatory. This was solved in Java long ago by requiring every method that may throw an exception to declare it in the method's signature and now Swift has adopted that protocol as well.

This means that the compiler will enforce that all possible errors are caught, which rules out the possibility of uncaught exceptions seen in Objective-C.

catch let unknownError {
 print("\(uknownError) occurred.")
}

After you have declared all catch statements, the compiler will force you to declare the above statement as it has no way of knowing that you have declared all possible error types (comparable to default in a switch statement). This will catch any remaining error that may have been thrown but not caught.

Note: this creates a slight performance advantage as well, considering that exceptions in Objective-C are resolved at run time versus being resolved at compile time for Swift 2.0.

Closing Thoughts

The use of enumerations for error handling provides a cleaner, safer and more graceful way to capture errors thrown during execution and creates the ability to return more meaningful error messages to the user.

One implication of the new error handling is that Swift 2.0 has begun to eliminate the NSError variable from method declarations. For example, the sendSynchronousRequest method declaration is as follows for Swift 1.2:

class func sendSynchronousRequest (request: NSURLRequest, returningResponse response: AutoreleasingUnsafeMutablePointer, error: NSErrorPointer) -> NSData?

However, that method is now declared as follows for Swift 2.0:

class func sendSynchronousRequest (request: NSURLRequest, returningResponse response: AutoreleasingUnsafeMutablePointer) throws -> NSData

This method now has two parameters and enforces error handling by adding the new throws statement to the method signature. Most importantly, the return type is now NSData rather than NSData?, ensuring that there is only one success path for a method call. This reduces the need for using the old if-let technique of checking return types or NSError variables, which with the use of all these cool new error handling features ultimately means…

  • Cleaner code
  • Safer code
  • Happier developers