Error Handling: From Objective-C to Swift and Back
Posted on November 3, 2015
Update 10/16:This post has been updated to Swift 3
The code for this article is available as a playground on Github or zipped.
Swift introduces error handling constructs like do/catch and try and its variants. In this article we’ll discuss this new feature, how it affects the base frameworks and how Swift modules that manage errors that way can be integrated in legacy Objective-C applications.
In the first releases of Swift, error handling was performed in a way that essentially mimicked how it has always been performed in Objective-C (the approach recommended by Apple).
In the good old days of Objective-C, when a method could fail with a recoverable error a NSError
pointer was added as the last parameter of the function, and in case of errors it was used to return a description of what happened.
Unrecoverable errors, that could prevent the application from being able to continue its execution normally, were sometimes handled with exceptions, that Objective-C also supported. Using NSError
for error handling was definitely the favorite approach.
Back to Swift, something like this in Objective-C:
NSError *error = nil;
NSArray *imageFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:@"./" error: &error];
was translated in Swift 1.2 as:
var error: NSError? = nil
let manager = NSFileManager.defaultManager()
var array = manager.contentsOfDirectoryAtPath(path:"./", error: error)
The contentsOfDirectoryAtPath
function was defined this way:
contentsOfDirectoryAtPath(path: String,error error: NSErrorPointer) -> [AnyObject]?
And NSErrorPointer was defined as:
typealias NSErrorPointer = AutoreleasingUnsafePointer<NSError?>
Even without going into the details of the types involved, it’s easy to see that in Swift 1.x there was nothing new about how errors were handled.
Everything changed with Swift 2.0.
Let’s see how error handling works (download the playground if you want to play with these examples) before discussing how a Swift component using the new constructs can be integrated in a legacy Objective-C application (hint: it’s not that hard).
Error Handling in Swift
The following simple example shows the basic syntax:
enum MyError : Error{
case AnError
case AnotherError
case JustAnotherError
}
func throwsError()throws ->Int {
throw MyError.AnotherError
}
do{
try throwsError()
}catch MyError.AnError {
print("AnError")
}catch MyError.AnotherError {
print("AnotherError") //AnotherError will be catched and printed
}catch{
print("Something else happened")
}
do{
do{
try throwsError()
}catch MyError.AnError {
print("AnError")
}
}catch MyError.AnotherError {
print("AnotherError") //AnotherError will be catched and printed
}catch{
print("Something else happened")
}
This example creates a custom error defining a new enum that conforms to the Error
protocol, each value will refer to a different condition for this error.
When an error needs to be returned from the function, we simply throw one of the values available in the MyError
enum and the function will complete its execution and return control to the caller, that will handle the error. Notice that the throwing function has to explicitly state that it could, under some circumstances, throw an error specifying the throws
keyword in its declaration.
To perform the actual error handling, the functions that could throw need to be preceded by a try
(or one of its alternatives as we’ll see) and need to be enclosed in a do/catch
block, that defines the context in which the errors will be managed.
Each catch will handle a specific error in its body. In the example above, our function throws always the same error that’s handled by the second catch, resulting in the name of the exception printed to console.
The do/catch does not have to cover all possible error conditions, if no catch block is able to handle an error, the error is simply propagated to the outer scope an so on, until a catch able to manage the error is found.
The nested do/catch of the next snippet shows that in action, the first do/catch handles only .AnError
errors, while the surrounding do/catch is able to handle the remaining alternatives.
But Swift error handling has a lot more to offer, as shown in this more complex example:
enum MyError2 : Error{
case GenericError
case DetailedError(String)
case NumericError(Int)
}
func throwsDetailedError()throws ->Int {
throw MyError2.DetailedError("Some details here")
}
func shouldNeverThrow()throws ->Int {
return 0
}
do{
defer{
//Clean up
}
try throwsDetailedError()
var value = try! shouldNeverThrow()
var imNil = try? throwsDetailedError()
}catch MyError2.GenericError {
print("GenericError")
}catch MyError2.DetailedError(let message) {
print("Error: \(message)") //Will print Error: Some details here
}catch MyError2.NumericError(let number) where number>0{
print("Error with id: "+String(number))
}catch{
print("Something else happened: "+String(describing:error))
}
There is much more going on in this example, this time our custom error can also have parameters for specific conditions and those parameters can then be bound to a variable in the catch clause. If no error or no binding variable is specified, like in the last catch, the error is automatically bound to an error
variable.
Another interesting thing is the use of defer
(I have placed it inside the do/catch but it doesn’t necessarily need to be there, it could be at the beginning of the current function), that provides the same functionality that is usually provided by a finally
block in Objective-C and other languages. The code contained in defer is guaranteed to be executed, no matter what, and because of this is usually used to perform mandatory clean-up operations.
Maybe more interesting it’s the ability to perform pattern matching as you are used to with switches, the code above contains a very simple example, just to show that it can be done, but everything available for switches is available here.
And then there is the first variation on try
.
Using try!
you are disabling error propagation for the shouldNeverThrow
function and wrapping it in a runtime assertion that will generate a runtime error (and crash your application) if the function throws an error. This allows you to ignore errors and their handling in those situations when you can be completely sure that even if a function is declared to throw, no error will actually ever be thrown.
The second and last variation of try is try?
, that handle errors producing an optional value that will contain the returned value if available or that will be nil in case of an error. You lose information about what kind of error was thrown (it could not be important) but you gain the ability to use the resulting optional in combination with all the statements that support them, from if let
to [map & flatMap](http://www.uraimo.com/2015/10/08/Swift2-map-flatmap-demystified/)
.
This is just an example of what you could do using map/flatMap:
var convertedInt = (try? shouldNeverThrow()).map{String($0)}
convertedInt
Not really that useful if taken alone, but with a lot of function possibly throwing errors, something like this or simply a wise use of optionals could help, in some cases, turn your code littered by do/catches into something more readable.
Base Frameworks and Error Handling
With the introduction of the new error handling in Swift 2.0, the contentsOfDirectoryAtPath
function and all the functions of the base frameworks that return NSErrors now have a slightly different prototype:
func contentsOfDirectoryAtPath(path: String) throws -> [String]
Every function that returned errors using an NSError
is now a function that throws but with one less parameter, and what the function throws is no other than the original NSError
. And if you check the documentation on NSError
, you’ll notice that among other protocols it now implements the Error
protocol.
And this alteration of the function prototype is the result of automatic bridging, as described in Apple’s “Using Swift with Cocoa and Objective-C”:
Swift automatically bridges between the Error type and the NSError class.
Objective-C methods that produce errors are imported as Swift methods that throw, and Swift methods that throw are imported as Objective-C methods that produce errors, according to Objective-C error conventions.
Let’s see what this means for Objective-C projects that need to integrate Swift code.
Swift Error Handling and Legacy Objective-C Applications
Suppose that you have an existing Objective-C project and that you plan to migrate gradually to Swift or that you simply want to extend an existing Objective-C application with a new Swift component, essentially turning your project in a mixed language application.
This section will use a simple OSX console application to show how this can be done, to follow along create a new console application or get the full sample on Github.
Once the project has been created, to start writing some Swift, simply add a new Swift file to your project, when asked if a bridging header needs to be created click No. With a bridging header you would be able to use Objective-C classes in Swift, in this section we’ll do the opposite.
Paste this code in your newly created Swift file:
import Foundation
@objc enum MyError:Int, Error{
case AnError
case AnotherError
}
public class MyClass:NSObject{
public func throwAnError() throws {
throw MyError.AnotherError
}
public func callMe(){
print("Someone called!")
}
}
We are going to invoke these two methods from Objective-C, but before that it’s important to know the module name of our project as shown under Product Module Name in Build Settings:
To access the Swift code we just wrote from Objective-C we just need to import an auto-generated header with a name that follows the format ModuleProductName-Swift.h (the actual file will be placed in the temporary build directory of your project and regenerated when needed).
Considering that our product name was ErrorHandling, the generated Swift header file will be named ErrorHandling-Swift.h.
The main.m
of your project should look like this:
#import <Foundation/Foundation.h>
#import "ErrorHandling-Swift.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyClass* c = [MyClass new];
NSError* err=nil;
[c throwAnErrorAndReturnError:&err];
NSLog(@"Domain:%@ Code:%d Message:%@",err.domain,err.code,err.localizedDescription);
[c callMe];
}
return 0;
}
In this example, we are simply creating a new instance of MyClass
and then invoking its two methods in sequence. There is not much to see other than the fact that the name of our throwAnError
function is now throwAnErrorAndReturnError
.
This is the result of the automatic bridging process described in the previous section.
The header that has been generated for us has this content:
SWIFT_CLASS("_TtC13ErrorHandling7MyClass")
@interface MyClass : NSObject
- (BOOL)throwAnErrorAndReturnError:(NSError * __nullable * __null_unspecified)error;
- (void)callMe;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end
typedef SWIFT_ENUM(NSInteger, MyError) {
MyErrorAnError = 0,
MyErrorAnotherError = 1,
};
static NSString * const MyErrorDomain = @"ErrorHandling.MyError";
As expected, the bridging process has added an NSError parameter to our error throwing function but also appended AndReturnError to its name following a common naming convention.
Also, since we added an @objc
modifier to our enum, but this was an optional step (not required to perform the bridging), we now have a convenient Objective-C enum that can be used in conjunction with the error code contained in the NSError.
Running out program produces the following output:
ErrorHandling[8104:413890] Domain:MyError Code:1 Message:The operation couldn’t be completed. (MyError error 1.)
Someone called!
Program ended with exit code: 0
Even if calling Swift code from Objective-C is not as straightforward as doing the opposite, the process is still painless, as promised at the beginning of the article error handling in a mixed language project it’s not that hard.
Did you like this article? Let me know on Twitter!