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?
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.
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.
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.
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.