Handle Exceptions in Swift
Baby's got a brand new bag
Unless you've been living under a rock, you're probably aware that Apple has released a new programming language called Swift. It's still a work-in-progress, moving towards 1.0. Designing a language and a standard library to go with it is no small task, so it's not surprising they haven't gotten around to filling in all the gaps yet.
Today, let's fill one of those gaps with some duct tape. Let's teach Swift to deal with exceptions.
First, here's what our Swift code looks like. As you might expect, we use closures to pass around functions.
try {
({
println("in try")
//code that might throw an exception
},
catch:{ ex in
println("caught \(ex.name): \(ex.description)")
},
finally: {
println("finally")
}
)}
Try takes a tuple, otherwise with multiple parameters we couldn't use the trailing closure syntax, which is what makes it look sorta like a control flow statement. I'm not sure if I like this sort of cute trick or not, but that's beside the point.
func try(maker: ()->(()->(), catch:((NSException!)->())?, finally:(()->())? )) {
let (action, catch, finally) = maker()
OSSExceptionHelper.tryInvokeBlock(action, catch, finally)
}
func try<T: AnyObject>(maker: ()->( ()->T, catch:((NSException!)->())?, finally: (()->())? )) -> T? {
let (action, catch, finally) = maker()
let result : AnyObject! = OSSExceptionHelper.tryInvokeBlockWithReturn(action, catch: { ex in
if let catchClause = catch {
catchClause(ex)
}
return nil
}, finally: finally)
if result {
return result as? T
} else {
return nil
}
}
func try<T: AnyObject>(maker: ()->( ()->T, catch:((NSException!)->T)?, finally: (()->())? )) -> T? {
let (action, catch, finally) = maker()
let result : AnyObject! = OSSExceptionHelper.tryInvokeBlockWithReturn(action, catch: catch, finally: finally)
if result {
return result as? T
} else {
return nil
}
}
func throw(name:String, message:String) {
OSSExceptionHelper.throwExceptionNamed(name, message: message)
}
Here we define three variations of try. The first one does not have a return value, so it's relatively easy. The second and third ones do have return values. The second form is handling the case where the catch clause does not return a value, the third form handles the case where it does. We assume T
is a reference type because trying to deal with the ABI issues around passing value types in Objective-C is way beyond the scope of what I want to deal with here.
One thing to note is you cannot return a value from the finally closure since it would always return that value, making the try closure pointless.
There's a lot of parens and punctuation in there, so let's break it down a bit: func try(maker: ()->(...))
. The try
function takes a parameter maker
that is itself a function that takes no arguments and returns a Tuple
of three items.
()->()
A closure that takes no arguments and returns no values.((NSException!)->())?
An optional closure; we surround the entire closure definition with parens, otherwise the compiler thinks we want a function that returns an optional empty tuple which makes no sense. The closure itself takes an implicitly unwrapped optional NSException. That works great since this closure will never be invoked unless we have an exception, so no boilerplate unwrapping required.(()->())?
Similar to #1, this is a closure that takes no arguments and returns no values, but we need extra parens similar to #2 so we can indicate that this closure is optional.
Now let's see OSSExceptionHelper:
@interface OSSExceptionHelper : NSObject
+ (void)tryInvokeBlock:(void(^)(void))tryBlock catch:(void(^)(NSException*))catchBlock finally:(void(^)(void))finallyBlock;
+ (id)tryInvokeBlockWithReturn:(id(^)(void))tryBlock catch:(id(^)(NSException*))catchBlock finally:(void(^)(void))finallyBlock;
+ (void)throwExceptionNamed:(NSString *)name message:(NSString *)message;
@end
@implementation OSSExceptionHelper
+ (void)tryInvokeBlock:(void(^)(void))tryBlock catch:(void(^)(NSException*))catchBlock finally:(void(^)(void))finallyBlock
{
NSAssert(tryBlock != NULL, @"try block cannot be null");
NSAssert(catchBlock != NULL || finallyBlock != NULL, @"catch or finally block must be provided");
@try {
tryBlock();
}
@catch (NSException *ex) {
if(catchBlock != NULL) {
catchBlock(ex);
}
}
@finally {
if(finallyBlock != NULL) {
finallyBlock();
}
}
}
+ (id)tryInvokeBlockWithReturn:(id(^)(void))tryBlock catch:(id(^)(NSException*))catchBlock finally:(void(^)(void))finallyBlock
{
NSAssert(tryBlock != NULL, @"try block cannot be null");
NSAssert(catchBlock != NULL || finallyBlock != NULL, @"catch or finally block must be provided");
id returnValue = nil;
@try {
returnValue = tryBlock();
}
@catch (NSException *ex) {
if(catchBlock != NULL) {
returnValue = catchBlock(ex);
}
}
@finally {
if(finallyBlock != NULL) {
finallyBlock();
}
}
return returnValue;
}
+ (void)throwExceptionNamed:(NSString *)name message:(NSString *)message;
{
[NSException raise:name format:message];
}
@end
This is fairly straightforward code. We just make use of the Objective-C exception handling to catch the exception and invoke the Swift closure in that case. That means the implicitly unwrapped NSException! is fine because we won't execute that closure unless an exception has occurred.
If you are using the try form that returns a value but the catch clause does not, remember to use catch:{ex -> () in }
, otherwise you'll get an ambiguous error because the compiler can't tell which overload of try you wanted.
So there we go; try/catch exception handling in Swift. The first entry in what I'm calling the Swift Non-Standard Library. I'll try to get it up on Github shortly.
This blog represents my own personal opinion and is not endorsed by my employer.