Home > Net >  Catching Both Objective-C and Swift Exceptions
Catching Both Objective-C and Swift Exceptions

Time:03-24

I don't actually have a question. But I ran into a problem about handling both Swift errors and Objective-C exceptions of type NSException using a single catch-block in Swift, similar to this:

do {
    try object1.throwingObjectiveCMethod()
    try object2.throwingSwiftMethod()
} catch {
    print("Error:", error)
}

I could not find an answer to this problem, so I thought I'd post it here in case someone else might run into it.

To step back, I needed to use some old Objective-C library from within Swift, which may throw NSExceptions that would need to be caught in Swift.

Let's say this library looks somewhat like this:

#import <Foundation/Foundation.h>

@interface MyLibrary : NSObject

- (void)performRiskyTask;

@end
@implementation MyLibrary

- (void)performRiskyTask {
    @throw [NSException exceptionWithName:NSInternalInconsistencyException
                                   reason:@"Something went wrong"
                                 userInfo:nil];
}

@end

Now, let's say I try to use this library in the following way:

do {
    let myLibrary = MyLibrary()
    try myLibrary.performRiskyTask()
} catch {
    print("Error caught:", error)
}

The Swift compiler is already letting me know that there are no throwing functions within the try expression and that the catch block is unreachable. And in fact, when I run the code, it crashes with a runtime error:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Something went wrong'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007ff8099861e3 __exceptionPreprocess   242
    1   libobjc.A.dylib                     0x00007ff8096e6c13 objc_exception_throw   48
    2   Exceptions                          0x0000000100003253 -[MyLibrary performRiskyTask]   83
    3   Exceptions                          0x00000001000035c2 main   66
    4   dyld                                0x000000010001951e start   462
)
libc  abi: terminating with uncaught exception of type NSException

As it is stated in the Swift docs, only the NSError ** pattern is translated to Swift's try-catch model. There is no built-in way to catch NSExceptions:

In Swift, you can recover from errors passed using Cocoa’s error pattern, as described above in Catch Errors. However, there’s no safe way to recover from Objective-C exceptions in Swift. To handle Objective-C exceptions, write Objective-C code that catches exceptions before they reach any Swift code. (https://developer.apple.com/documentation/swift/cocoa_design_patterns/handling_cocoa_errors_in_swift)

As pointed out in another answer (see here), this issue can be resolved using the following generic exception handler:

#import <Foundation/Foundation.h>

@interface ObjC : NSObject

  (BOOL)catchException:(void (^)(void))tryBlock error:(NSError **)error;

@end
@implementation ObjC

  (BOOL)catchException:(void (^)(void))tryBlock error:(NSError **)error {
    @try {
        tryBlock();
        return YES;
    } @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

Rewriting my Swift code (and importing ObjC.h in my bridging header) …

do {
    let myLibrary = MyLibrary()
    try ObjC.catchException {
        myLibrary.performRiskyTask()
    }
} catch {
    print("Error caught:", error)
}

…, the exception is now being caught:

Error caught: Error Domain=NSInternalInconsistencyException Code=-1 "Something went wrong" UserInfo={CallStackSymbols=(
    0   CoreFoundation                      0x00007ff8099861e3 __exceptionPreprocess   242
    1   libobjc.A.dylib                     0x00007ff8096e6c13 objc_exception_throw   48
    2   Exceptions                          0x0000000100002c33 -[MyLibrary performRiskyTask]   83
    3   Exceptions                          0x0000000100003350 $s10ExceptionsyycfU_   32
    4   Exceptions                          0x00000001000033d8 $sIeg_IeyB_TR   40
    5   Exceptions                          0x0000000100002c9f  [ObjC catchException:error:]   95
    6   Exceptions                          0x0000000100003069 main   265
    7   dyld                                0x000000010001d51e start   462
), NSLocalizedDescription=Something went wrong, NSUnderlyingError=Something went wrong}
Program ended with exit code: 0

So this works great.

But now let's say, my library calls are nested inside other Swift functions which may throw other, Swift-native errors:

func performTasks() throws {
    try performOtherTask()
    useLibrary()
}

func performOtherTask() throws {
    throw MyError.someError(message: "Some other error has occurred.")
}

func useLibrary() {
    let myLibrary = MyLibrary()
    myLibrary.performRiskyTask()
}

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

Now of course, I could surround every call to my library by an ObjC.catchException block or even build a Swift wrapper around this library. But instead, I would like to catch all exceptions at a single spot without having to write extra code for every method call:

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

This code doesn't compile, however, because the closure passed to the ObjC.catchException method shouldn't throw:

Invalid conversion from throwing function of type '() throws -> ()' to non-throwing function type '() -> Void'

CodePudding user response:

To resolve the issue, replace the void (^tryBlock)(void) by a non-escaping void (^tryBlock)(NSError **) and mark the Objective-C method as "refined for Swift" (similar to this answer):

#import <Foundation/Foundation.h>

@interface ObjC : NSObject

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

@end

Then pass the error pointer to the tryBlock and change the return value depending on whether an error occurred:

@implementation ObjC

  (BOOL)catchException:(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

Then "refine" this method using the following extension:

extension ObjC {
    static func catchException(_ block: () throws -> Void) throws {
        try __catchException { (errorPointer: NSErrorPointer) in
            do {
                try block()
            } catch {
                errorPointer?.pointee = error as NSError
            }
        }
    }
}

Now, the Swift code does compile:

do {
    try ObjC.catchException {
        try performTasks()
    }
    print("No errors.")
} catch {
    print("Error caught:", error)
}

And it catches the Swift error:

Error caught: someError(message: "Some other error has occurred.")
Program ended with exit code: 0

Similarly, it will also still catch the NSException:

func performOtherTask() throws {
    //throw MyError.someError(message: "Some other error has occurred.")
}
Error caught: Error Domain=NSInternalInconsistencyException Code=-1 "Something went wrong" UserInfo={CallStackSymbols=(
    0   CoreFoundation                      0x00007ff8099861e3 __exceptionPreprocess   242
    1   libobjc.A.dylib                     0x00007ff8096e6c13 objc_exception_throw   48
    2   Exceptions                          0x0000000100002643 -[MyLibrary performRiskyTask]   83
    3   Exceptions                          0x00000001000030fa $s10Exceptions10useLibraryyyF   58
    4   Exceptions                          0x0000000100002cfa $s10Exceptions12performTasksyyKF   42
    5   Exceptions                          0x0000000100002c9f $s10ExceptionsyyKXEfU_   15
    6   Exceptions                          0x00000001000031b8 $sSo4ObjCC10ExceptionsE14catchExceptionyyyyKXEKFZySAySo7NSErrorCSgGSgXEfU_   56
    7   Exceptions                          0x000000010000339c $sSAySo7NSErrorCSgGSgIgy_AEIegy_TR   12
    8   Exceptions                          0x000000010000340c $sSAySo7NSErrorCSgGSgIegy_AEIyBy_TR   28
    9   Exceptions                          0x00000001000026b3  [ObjC catchException:error:]   99
    10  Exceptions                          0x0000000100002e93 $sSo4ObjCC10ExceptionsE14catchExceptionyyyyKXEKFZ   371
    11  Exceptions                          0x00000001000029ce main   46
    12  dyld                                0x000000010001d51e start   462
), NSLocalizedDescription=Something went wrong, NSUnderlyingError=Something went wrong}
Program ended with exit code: 0

And, of course, it will just run through if no exception is thrown at all:

- (void)performRiskyTask {
//    @throw [NSException exceptionWithName:NSInternalInconsistencyException
//                                   reason:@"Something went wrong"
//                                 userInfo:nil];
}
No errors.
Program ended with exit code: 0

If this has been posted anywhere else and I have just completely wasted my time, feel free to let me know.

  • Related