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.