Got a really good question today: How does Swift 2.0’s new error handling mechanisms work with asynchronous execution. I don’t have an authoritative answer but I do have a some thoughts to share.
Asynchrony and Throw
Many asynchronous Cocoa calls use completion blocks. You place a request and the completion block executes when the request completes, regardless of whether that request succeeded or failed.
You don’t try these asynchronous calls. There’s no point as you’re passing a handler that understands and manages error states. Instead, you work with those responses in the handler. The handler provides a data and error argument parameter tuple as in this Social framework example:
slrequest.performRequestWithHandler { (NSData!, NSHTTPURLResponse!, NSError!) -> Void in // code handling }
Swift 2.0’s redesigned error system doesn’t affect the externals of this call. There’s nothing to catch from this request. You do not have to call it with try. Inside the handler is another matter.
Using Throwing Elements within Closures
This following implementation places an asynchronous request. It converts some returned data to a string and saves that string to a file. Because the writeToFile:atomically:encoding: function uses the throws annotation, you must call it using try. This sets up an error-path microenvironment within the handler closure.
slrequest.performRequestWithHandler { (data: NSData!, response: NSHTTPURLResponse!, error: NSError!) -> Void in if let string = NSString(data: data, encoding: NSUTF8StringEncoding) { try string.writeToFile("/tmp/test.txt", atomically: true, encoding: NSUTF8StringEncoding) } }
At this point, you encounter an issue. The compiler complains about an error, specifically:
invalid conversion from throwing function of type ‘(NSData!, NSHTTPURLResponse!, NSError!) throws -> Void’ to non-throwing function type ‘@convention(block) (NSData!, NSHTTPURLResponse!, NSError!) -> Void’
The Social request doesn’t work with throwing closures such as (NSData! NSHTTPURLResponse!, NSError!) throws -> Void. Its handler parameter isn’t set up for them. To mitigate this issue, consume any thrown errors within the block This converts the throwing closure to a non-throwing closure and enables it to be passed as the handler parameter.
In fact, as far as I can tell, you cannot use external error pathways for any asynchronous calls, whether they are used for completions, handlers, operations, or dispatch.
Transforming a Throws-ing Closure into a non-Throws-ing Closure
To fix the handler, add a valid pathway for any errors. The do-catch mechanism transforms the handler parameter in the performRequestWithHandler call from one that throws to one that does not. You can now pass it to the performRequestWithHandler: method.
slrequest.performRequestWithHandler { (data: NSData!, response: NSHTTPURLResponse!, error: NSError!) -> Void in do { if let string = NSString(data: data, encoding: NSUTF8StringEncoding) { try string.writeToFile("/tmp/test.txt", atomically: true, encoding: NSUTF8StringEncoding) } } catch {print("Unable to complete request. \(error)")} }
By providing a full pathway for the errors to follow, you ensure the error is propagated to a valid and exhaustive catch clause.
GCD
You see a similar issue when working with Grand Central Dispatch. Like the Social request, GCD executes a closure asynchronously and is not set up to accept one whose signature includes throwing.
In the following example, the outer function dispatches a block that throws. To succeed, it must try the block and catch any errors. This exhaustive catch prevents those errors from propagating further and transforms the inner closure into one without a throws signature.
Without this overhead, the internal dispatch_after call cannot compile. It expects a non-throwing dispatch_block_t argument that cannot be provided if the closure contains any remaining error pathways.
public func dispatch_after(delay: Double, block: () throws -> Void) { dispatch_after( dispatch_time(DISPATCH_TIME_NOW, numericCast(UInt64(delay * Double(NSEC_PER_SEC)))), dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { do { try block() } catch { print("Error during async execution") print(error) } } ) }
Forced Try
Swift offers another way to de-throws a block, and that is with try!. This is called a “forced try” expression and is written with a simple exclamation point after the try keyword.
public func dispatch_after(delay: Double, block: () throws -> Void) { dispatch_after( dispatch_time(DISPATCH_TIME_NOW, numericCast(UInt64(delay * Double(NSEC_PER_SEC)))), dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { try! block() } ) }
Apple writes, “Calling a throwing function or method with try! disables error propagation and wraps the call in a run-time assertion that no error will be thrown. If an error actually is thrown, you’ll get a runtime error.” They’re simpler to write but produce uglier fails (not that fails are usually beautiful in any way.)
Conclusion
Bottom line. While errors can be thrown from one scope to another, as far as I can tell those scopes must inhabit the same thread. If I’m reading this right, a complete error handling pathway must be provided for each execution thread.
If you’re working with asynchronous closures, you’ll need to supply ones that do not use throwing signatures. Convert your closures to non-throwing versions by providing exhaustive catch clauses.
3 Comments
One would hope that something like a future is in Swift’s future, to allow return type (and through that, exception) propagation across threads.
I heavily use async operations in my app, and have used this method for passing around errors: https://pixelspark.nl/2015/swift-gems-for-creating-reliable-concurrent-desktop-apps. Basically an optional type with room to put an error, with some syntactic sugar to make it easy to use.
[…] Erica Sadun: […]