swiftobjective-cexception

Catching Both Objective-C and Swift Exceptions with Support for Return Values


I have a question on catching both Swift errors and Objective-C exceptions using a single do-catch statement.

Let's say I have a function which invokes a bunch of other functions that may either throw a Swift Error or an Objective-C NSException:

func performTasks() throws {
    functionThrowingNSException()
    try functionThrowingSwiftError()
    functionThrowingNSException()
    try functionThrowingSwiftError()
    …
}

The Swift function may look somewhat like this:

func functionThrowingSwiftError() throws {
    throw MyError.someError(message: "Swift error")
}

enum MyError: Error {
    case someError(message: String)
}

And the Objective-C part may look like this:

func functionThrowingNSException() {
    let myLibrary = MyLibrary()
    myLibrary.performRiskyTask()
}
@interface MyLibrary : NSObject

- (void)performRiskyTask;

@end
@implementation MyLibrary

- (void)performRiskyTask {
    @throw [NSException exceptionWithName:NSInternalInconsistencyException
                                   reason:@"Objective-C exception"
                                 userInfo:nil];
}

@end

Now I would like to catch both types of exceptions in a single catch-block. If I try this, however …

do {
    try performTasks()
} catch {
    print("Error caught:", error)
}

…, the app crashes at runtime:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Objective-C exception'

As pointed out in a previous post, this issue can be resolved using the following code:

@interface ExceptionHandling : NSObject

+ (BOOL)tryVoidObjC:(void (NS_NOESCAPE ^)(NSError **))tryBlock error:(NSError **)error NS_REFINED_FOR_SWIFT;

@end
@implementation ExceptionHandling

+ (BOOL)tryVoidObjC:(void (NS_NOESCAPE ^)(NSError **))tryBlock error:(NSError **)error {
    @try {
        tryBlock(error);
        return error == NULL || *error == nil;
    } @catch (NSException *exception) {
        if (error != NULL) {
            *error = [NSError errorWithDomain:exception.name code:-1 userInfo:@{
                NSUnderlyingErrorKey: exception,
                NSLocalizedDescriptionKey: exception.reason,
                @"callStackSymbols": exception.callStackSymbols
            }];
        }
        return NO;
    }
}

@end
extension ExceptionHandling {
    static func tryVoidObjC(_ block: () throws -> Void) throws {
        try __tryVoidObjC { (errorPointer: NSErrorPointer) in
            do {
                try block()
            } catch {
                errorPointer?.pointee = error as NSError
            }
        }
    }
}

This way, I can convert the NSException to a Swift error and catch it:

do {
    try performTasks()
} catch {
    print("Error caught:", error)
}
func performTasks() throws {
    try ExceptionHandling.tryVoidObjC {
        functionThrowingNSException()
        try functionThrowingSwiftError()
        functionThrowingNSException()
        try functionThrowingSwiftError()
    }
}
Error caught: Error Domain=NSInternalInconsistencyException Code=-1 "Objective-C exception" UserInfo={callStackSymbols =(…), NSLocalizedDescription=Objective-C exception, NSUnderlyingError=Objective-C exception}

Now, the functionThrowingNSException() is a void function. What if I want to use an Objective-C method that can either throw an NSException or return a value? So something like this:

do {
    let answer = try performTasks()
    print("The answer is \(answer).")
} catch {
    print("Error caught:", error)
}
func performTasks() throws -> Int {
    return try ExceptionHandling.tryObjC {
        functionThrowingNSException()
        try functionThrowingSwiftError()
        functionThrowingNSException()
        try functionThrowingSwiftError()
        return ultimateAnswer()
    }
}

func ultimateAnswer() -> Int {
    let myLibrary = MyLibrary()
    return myLibrary.ultimateAnswer()
}
@interface MyLibrary : NSObject

- (NSInteger)ultimateAnswer;

@end
@implementation MyLibrary

- (NSInteger)ultimateAnswer {
    @throw [NSException exceptionWithName:NSGenericException
                                   reason:@"Computation timed out"
                                 userInfo:nil];
}

@end

How would I go about doing that?

First approach:

It's important to understand how Swift translates Cocoa's error pattern:

If the last non-block parameter of an Objective-C method is of type NSError **, Swift replaces it with the throws keyword, to indicate that the method can throw an error.

[…]

If an error producing Objective-C method returns a BOOL value to indicate the success or failure of a method call, Swift changes the return type of the function to Void. Similarly, if an error producing Objective-C method returns a nil value to indicate the failure of a method call, Swift changes the return type of the function to a nonoptional type.

Otherwise, if no convention can be inferred, the method is left intact.

(https://developer.apple.com/documentation/swift/cocoa_design_patterns/about_imported_cocoa_error_parameters)

For this reason, the +[ExceptionHandling tryVoidObjC:error:] method (if it weren't "refined for Swift"), is translated to func tryObjC(_ tryBlock: ((NSErrorPointer) -> Void)!) throws.

We need to use a boolean return value for this Objective-C method, because if we were to use a void return type, no convention could be inferred and the method would instead be translated to the non-throwing function func tryObjC(_ tryBlock: ((NSErrorPointer) -> Void)!, error: NSErrorPointer).

Similarly, we cannot use an integer return type because this would translate the method to func tryObjC(_ tryBlock: ((NSErrorPointer) -> Int)!, error: NSErrorPointer) -> Int.

So one way to solve the problem would be to return an NSNumber instance and take care of converting it from and to an integer in the Swift refinement:

@interface ExceptionHandling : NSObject

+ (NSNumber *)tryObjC:(NSNumber * (NS_NOESCAPE ^)(NSError **))tryBlock error:(NSError **)error NS_REFINED_FOR_SWIFT;

@end
@implementation ExceptionHandling

+ (NSNumber *)tryObjC:(NSNumber * (NS_NOESCAPE ^)(NSError **))tryBlock error:(NSError **)error {
    @try {
        return tryBlock(error);
    } @catch (NSException *exception) {
        if (error != NULL) {
            *error = [NSError errorWithDomain:exception.name code:-1 userInfo:@{
                NSUnderlyingErrorKey: exception,
                NSLocalizedDescriptionKey: exception.reason,
                @"callStackSymbols": exception.callStackSymbols
            }];
        }
        return nil;
    }
}

@end
extension ExceptionHandling {
    static func tryObjC(_ block: () throws -> Int) throws -> Int {
        let number = try __tryObjC { errorPointer in
            do {
                let integer = try block()
                return NSNumber(integerLiteral: integer)
            } catch {
                errorPointer?.pointee = error as NSError
                return nil
            }
        }
        return number.intValue
    }
}

This solution does actually work. The code will compile and, if those other methods don't interrupt execution by throwing errors …

func functionThrowingNSException() {
    //let myLibrary = MyLibrary()
    //myLibrary.performRiskyTask()
}

func functionThrowingSwiftError() throws {
    //throw MyError.someError(message: "Swift error")
}

…, the exception will be caught:

Error caught: Error Domain=NSGenericException Code=-1 "Computation timed out" UserInfo={callStackSymbols=(…), NSLocalizedDescription=Computation timed out, NSUnderlyingError=Computation timed out}

Likewise, the actual return value is printed if the library method doesn't throw:

@implementation MyLibrary

- (NSInteger)ultimateAnswer {
    return 42;
}

@end
The answer is 42.

This solution is not ideal, however, because it would need to be re-implemented for every return type. It would be preferable for the tryObjC method to have a generic return type.


Solution

  • The first step to dealing with generic return types is to replace NSNumber * by id in the Objective-C class:

    @interface ExceptionHandling : NSObject
    
    + (id)tryObjC:(id (NS_NOESCAPE ^)(NSError **))tryBlock error:(NSError **)error NS_REFINED_FOR_SWIFT;
    
    @end
    
    @implementation ExceptionHandling
    
    + (id)tryObjC:(id (NS_NOESCAPE ^)(NSError **))tryBlock error:(NSError **)error {
        @try {
            return tryBlock(error);
        } @catch (NSException *exception) {
            if (error != NULL) {
                *error = [NSError errorWithDomain:exception.name code:-1 userInfo:@{
                    NSUnderlyingErrorKey: exception,
                    NSLocalizedDescriptionKey: exception.reason,
                    @"callStackSymbols": exception.callStackSymbols
                }];
            }
            return nil;
        }
    }
    
    @end
    

    Next, provide a generic Swift method as the refinement:

    extension ExceptionHandling {
        static func tryObjC<T>(_ block: () throws -> T) throws -> T {
            return try __tryObjC { errorPointer in
                do {
                    return try block()
                } catch {
                    errorPointer?.pointee = error as NSError
                    return nil
                }
            } as! T
        }
    }
    

    The code will compile and the result will be printed (or the NSException will be caught):

    do {
        let answer = try performTasks()
        print("The answer is \(answer).")
    } catch {
        print("Error caught:", error)
    }
    
    func performTasks() throws -> Int {
        return try ExceptionHandling.tryObjC {
            functionThrowingNSException()
            try functionThrowingSwiftError()
            functionThrowingNSException()
            try functionThrowingSwiftError()
            return ultimateAnswer()
        }
    }
    
    The answer is 42.
    

    The compiler even took care of "autoboxing" the NSInteger to an NSNumber * automatically.

    Remark:

    The +[ExceptionHandling tryObjC:error:] method is translated to func __tryObjC(_ tryBlock: ((NSErrorPointer) -> Any?)!) throws -> Any.

    Notice that while the tryBlock seems to support an optional return value, the return value of the __tryObjC(_:) method must be non-optional. This is in line with the excerpt from the Swift docs saying that Swift changes the return type of the Objective-C method to a non-optional type.

    If we actually were to supply nil as the return value of the tryBlock

    @interface MyLibrary : NSObject
    
    - (NSString *)ultimateAnswer;
    
    @end
    
    @implementation MyLibrary
    
    - (NSString *)ultimateAnswer {
        return nil;
    }
    
    @end
    
    func performTasks() throws -> String? {
        return try ExceptionHandling.tryObjC {
            functionThrowingNSException()
            try functionThrowingSwiftError()
            functionThrowingNSException()
            try functionThrowingSwiftError()
            return ultimateAnswer()
        }
    }
    
    func ultimateAnswer() -> String? {
        let myLibrary = MyLibrary()
        return myLibrary.ultimateAnswer()
    }
    

    …, we would get a runtime error saying …

    Could not cast value of type 'NSNull' to 'NSString'
    

    … because the forced cast try __tryObjC { … } as! T would fail.

    So returning nil but not throwing an NSException is not a pattern that is supported by this solution (just like returning nil but not setting the error parameter is not following Cocoa's error convention).

    EDIT: Please understand the implications of this approach pointed out in the comments below.