Swift 2 try - catch under the hood

Swift 2 introduces a new error handling mechanism that integrates seamlessly with existing Cocoa frameworks. In the introductory talk, it was explicitly stated that Swift 2 does not use exceptions to propagate errors but rather a (much faster) ‘return value’ mechanims (my words, I don’t quite remember what it was called).

Curious as to what this might look like under the hood and remembering that the SIL can be inspected quite easily, I poked around a bit. Starting from

func doStuff() throws -> String {

we can dump the SIL as follows:

swiftc -emit-sil test.swift > test.sil

This is a simple text file and within it, we find what our method signature is compiled into:

%11 = function_ref @_TF8Contents7doStuffFzT_SS : $@convention(thin) () -> (@owned String, @error ErrorType) // user: %12

Don’t be put off by the mangled name _TF8Contents7doStuffFzT_SS for doStuff. It’s an encoding of module name, method name, parameters and return types into a single string. What we also see is that apparently the error handling turns our method into:

func doStuff() -> (String, ErrorType)

So it looks like it is using something quite similar to the Result<T> approach that’s become quite common for error handling.

At the call site of doStuff we see the following:

try_apply %11() : $@convention(thin) () -> (@owned String, @error ErrorType), normal bb1, error bb4 // id: %12
bb1(%13 : $String):                               // Preds: bb0
bb3:                                              // Preds: bb4
  debug_value %44 : $ErrorType  // let error      // id: %30
bb4(%44 : $ErrorType):                            // Preds: bb0
  br bb3                                          // id: %45

which to my (untrained) eye looks like a switch routing to bb1 for the normal case and through bb4 and then to bb3 for the error. Note the implicit let error making the variable available to the catch clause.

Repeating the exercise with an NSError based Cocoa method shows something slightly different. Looking at the SIL for

do {
    let fm = NSFileManager.defaultManager()
    let files = try fm.contentsOfDirectoryAtPath("/foo")
} catch {
    print("An error occurred.")

we see

%17 = class_method [volatile] %14 : $NSFileManager, #NSFileManager.contentsOfDirectoryAtPath!1.foreign : NSFileManager -> (String) throws -> [String] , $@convention(objc_method) (NSString, AutoreleasingUnsafeMutablePointer<Optional<NSError>>, NSFileManager) -> @autoreleased Optional<NSArray> // user: %36

To me this looks like it is mapping to the ‘old’ signature of

fm.contentsOfDirectoryAtPath("/foo", &error)

behind the scenes automatically. Further down this is picked up in

%36 = apply %17(%25, %33, %14) : $@convention(objc_method) (NSString, AutoreleasingUnsafeMutablePointer<Optional<NSError>>, NSFileManager) -> @autoreleased Optional<NSArray> // users: %37, %44
  switch_enum %36 : $Optional<NSArray>, case #Optional.Some!enumelt.1: bb1, case #Optional.None!enumelt: bb4 // id: %44

So in this case it doesn’t look like the error is returned via a tuple as in our first example but rather just switches on the NSError coming out of the Cocoa framework call as we know it.

From this point on both mechanisms appear to essentially do the same thing, with the slight twist that the NSError is converted to ErrorType:

// function_ref swift_convertNSErrorToErrorType
  %79 = function_ref @swift_convertNSErrorToErrorType : $@convention(thin) (@owned Optional<NSError>) -> @owned ErrorType // user: %80

I’m not sure yet how much I like Swift’s new error handling but it is certainly nicer than NSErrorPointer and interesting to see how it’s being handled behind the scenes. I would guess it is much easier to tweak the mechanism given that so much of it is being handled at the SIL and standard library layer.