Catch Objective-C exception in Swift
see this answer:
//
// ExceptionCatcher.h
//
#import <Foundation/Foundation.h>
NS_INLINE NSException * _Nullable tryBlock(void(^_Nonnull tryBlock)(void)) {
@try {
tryBlock();
}
@catch (NSException *exception) {
return exception;
}
return nil;
}
Catching Both Objective-C and Swift Exceptions
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
EDIT: To support non-void methods throwing NSExceptions, see this post.
Exception is not being caught
Swift converts Objective-C methods with nullable returns and trailing NSError**
parameters to methods that throw in Swift. But, in Objective-C, you can also throw exceptions. These are distinct from NSError
s and Swift does not catch them. In fact there is no way to catch them in Swift. You would have to write an Objective-C wrapper that catches the exception and passes it back in some way Swift can handle.
You can find this in the Apple document Handling Cocoa Errors in Swift in the Handle Exceptions in Objective-C Only section.
So it turns out that you can catch it, but it is worthwhile considering whether you should (see comments from @Sulthan below). To the best of my knowledge most Apple frameworks are not exception-safe (see: Exceptions and the Cocoa Frameworks) so you can't just catch an exception and continue on as if nothing happened. Your best bet is save what you can and exit as soon as possible. Another issue to consider is that unless you rethrow the exception, frameworks such as Crashlytics won't report it back to you. So, if you did decide to catch it you should log it and/or rethrow it so that you know that it is happening.
Catching NSException in Swift
Here is some code, that converts NSExceptions to Swift 2 errors.
Now you can use
do {
try ObjC.catchException {
/* calls that might throw an NSException */
}
}
catch {
print("An error ocurred: \(error)")
}
ObjC.h:
#import <Foundation/Foundation.h>
@interface ObjC : NSObject
+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error;
@end
ObjC.m
#import "ObjC.h"
@implementation ObjC
+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
@try {
tryBlock();
return YES;
}
@catch (NSException *exception) {
*error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo];
return NO;
}
}
@end
Don't forget to add this to your "*-Bridging-Header.h":
#import "ObjC.h"
Swift Catch Runtime Exceptions
To catch Obj-C exceptions in Swift, I am using a simple Obj-C class:
#import "ObjC2Swift.h"
@implementation ObjC
+ (id)catchException:(id(^)())tryBlock error:(__autoreleasing NSError **)error {
@try {
id result = tryBlock();
return result;
}
@catch (NSException *exception) {
if (error) {
*error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo];
}
return nil;
}
}
@end
In Swift called as
let result = try? ObjC.catchException { ... dangerous code here ... }
You might need a different variant for blocks that don't return anything.
Not to be abused. Obj-C exception are evil and I am using this only because I need a library that uses them.
How can I throw an NSError from a Swift class and catch it in an Objective-C class?
The problem is that a Swift Error / Objective-C NSError is not an NSException. You are configured to catch NSExceptions but that is irrelevant.
The way to "catch" an NSError in Objective-C when Swift throws an Error is by indirection with the NSError**
parameter, just as it always has been.
NSError* err = nil;
BOOL ok = [self.netServiceManager requestHelloPageAndReturnError:&err];
if (ok) {
// proceed normally
} else {
// you got an error, it is sitting in `err`
}
(Notice how Swift supplies a BOOL result exactly so you can implement the correct pattern.)
Related Topics
Swift: How to Fully Strip Internal/Inline Symbols
How to Record Video in Realitykit
Detect Ethernet/Wifi Network Change
Swift Calling Static Methods: Type(Of: Self) VS Explicit Class Name
Stop Objects from Colliding Using Spritekit
Is Swift Inout Parameter a Variable or a Pointer
Swiftui - How to Set the Title of a Navigationview to Large Title (Or Small)
Select Next Nstextfield with Tab Key in Swift
How to Extract Image from Lplinkview in Linkpresentation Framework
How to Pass Structure by Reference
Get the Current Position of Scrollview in Swiftui
Swiftui MACos Nswindow Instance
Swift Only -- Reading from Nsinputstream
Swift Check Type Against a Generic Type
How to Get Current Location with Swiftui
List All Available Audio Devices