Catching NSException in Swift

30,572

Solution 1

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"

Solution 2

What I suggest is to make an C function that will catch the exception and return a NSError instead. And then, use this function.

The function could look like this:

NSError *tryCatch(void(^tryBlock)(), NSError *(^convertNSException)(NSException *))
{
    NSError *error = nil;
    @try {
        tryBlock();
    }
    @catch (NSException *exception) {
        error = convertNSException(exception);
    }
    @finally {
        return error;
    }
}

And with a little bridging help, you'll just have to call:

if let error = tryCatch(task.launch, myConvertFunction) {
    print("An exception happened!", error.localizedDescription)
    // Do stuff
}
// Continue task

Note: I didn't really test it, I couldn't find a quick and easy way to have Objective-C and Swift in a Playground.

Solution 3

TL;DR: Use Carthage to include https://github.com/eggheadgames/SwiftTryCatch or CocoaPods to include https://github.com/ravero/SwiftTryCatch.

Then you can use code like this without fear it will crash your app:

import Foundation
import SwiftTryCatch

class SafeArchiver {

    class func unarchiveObjectWithFile(filename: String) -> AnyObject? {

        var data : AnyObject? = nil

        if NSFileManager.defaultManager().fileExistsAtPath(filename) {
            SwiftTryCatch.tryBlock({
                data = NSKeyedUnarchiver.unarchiveObjectWithFile(filename)
                }, catchBlock: { (error) in
                    Logger.logException("SafeArchiver.unarchiveObjectWithFile")
                }, finallyBlock: {
            })
        }
        return data
    }

    class func archiveRootObject(data: AnyObject, toFile : String) -> Bool {
        var result: Bool = false

        SwiftTryCatch.tryBlock({
            result =  NSKeyedArchiver.archiveRootObject(data, toFile: toFile)
            }, catchBlock: { (error) in
                Logger.logException("SafeArchiver.archiveRootObject")
            }, finallyBlock: {
        })
        return result
    }
}

The accepted answer by @BPCorp works as intended, but as we discovered, things get a little interesting if you try to incorporate this Objective C code into a majority Swift framework and then run tests. We had problems with the class function not being found (Error: Use of unresolved identifier). So, for that reason, and just general ease of use, we packaged it up as a Carthage library for general use.

Strangely, we could use the Swift + ObjC framework elsewhere with no problems, it was just the unit tests for the framework that were struggling.

PRs requested! (It would be nice to have it a combo CocoaPod & Carthage build, as well as have some tests).

Solution 4

As noted in comments, that this API throws exceptions for otherwise-recoverable failure conditions is a bug. File it, and request an NSError-based alternative. Mostly the current state of affairs is an anachronism, as NSTask dates to back before Apple standardized on having exceptions be for programmer errors only.

In the meantime, while you could use one of the mechanisms from other answers to catch exceptions in ObjC and pass them to Swift, be aware that doing so isn't very safe. The stack-unwinding mechanism behind ObjC (and C++) exceptions is fragile and fundamentally incompatible with ARC. This is part of why Apple uses exceptions only for programmer errors — the idea being that you can (theoretically, at least) sort out all the exception cases in your app during development, and have no exceptions occurring in your production code. (Swift errors or NSErrors, on the other hand, can indicate recoverable situational or user errors.)

The safer solution is to foresee the likely conditions that could cause an API to throw exceptions and handle them before calling the API. If you're indexing into an NSArray, check its count first. If you're setting the launchPath on an NSTask to something that might not exist or might not be executable, use NSFileManager to check that before you launch the task.

Solution 5

The version improves the answer above to return the actual detailed exception message.

@implementation ObjC

+ (BOOL)tryExecute:(nonnull void(NS_NOESCAPE^)(void))tryBlock error:(__autoreleasing NSError * _Nullable * _Nullable)error {
   @try {
      tryBlock();
      return YES;
   }
   @catch (NSException *exception) {
      NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init];
      if (exception.userInfo != NULL) {
         userInfo = [[NSMutableDictionary alloc] initWithDictionary:exception.userInfo];
      }
      if (exception.reason != nil) {
         if (![userInfo.allKeys containsObject:NSLocalizedFailureReasonErrorKey]) {
            [userInfo setObject:exception.reason forKey:NSLocalizedFailureReasonErrorKey];
         }
      }
      *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:userInfo];
      return NO;
   }
}

@end

Example usage:

      let c = NSColor(calibratedWhite: 0.5, alpha: 1)
      var r: CGFloat = 0
      var g: CGFloat = 0
      var b: CGFloat = 0
      var a: CGFloat = 0
      do {
         try ObjC.tryExecute {
            c.getRed(&r, green: &g, blue: &b, alpha: &a)
         }
      } catch {
         print(error)
      }

Before:

Error Domain=NSInvalidArgumentException Code=0 "(null)"

After:

Error Domain=NSInvalidArgumentException Code=0 
"*** -getRed:green:blue:alpha: not valid for the NSColor NSCalibratedWhiteColorSpace 0.5 1; need to first convert colorspace." 
UserInfo={NSLocalizedFailureReason=*** -getRed:green:blue:alpha: not valid for the NSColor NSCalibratedWhiteColorSpace 0.5 1; need to first convert colorspace.}
Share:
30,572

Related videos on Youtube

silyevsk
Author by

silyevsk

Updated on July 08, 2022

Comments

  • silyevsk
    silyevsk almost 2 years

    The following code in Swift raises NSInvalidArgumentException exception:

    task = NSTask()
    task.launchPath = "/SomeWrongPath"
    task.launch()
    

    How can I catch the exception? As I understand, try/catch in Swift is for errors thrown within Swift, not for NSExceptions raised from objects like NSTask (which I guess is written in ObjC). I'm new to Swift so may be I'm missing something obvious...

    Edit: here's a radar for the bug (specifically for NSTask): openradar.appspot.com/22837476

    • Martin R
      Martin R over 8 years
      Unfortunately, you can't catch Objective-C exceptions in Swift, see for example stackoverflow.com/questions/24023112/…. One might consider it a bug that NSTask throws exceptions instead of returning errors and you could file a bug report, but I doubt that Apple will change the API.
    • silyevsk
      silyevsk over 8 years
      Thanks @MartinR. I think it's either a bug in the API, or Swift should provide a mechanism to catch ObjC exceptions (or better the both)... Anyways, I've opened a bug (openradar.appspot.com/22837476), though I guess there are many more API methods with the same problem
    • matt
      matt almost 7 years
      The example for me was NSPredicate(fromMetadataQueryString:). This is supposed to be an init?, so if the string is bad it is probably intended to return nil, but in fact it just crashes with an NSException.
    • Top-Master
      Top-Master over 2 years
      Good question! But if you throw the NSException yourself, then easily see how to create NSError (and throw that, instead of NSException)
  • silyevsk
    silyevsk over 8 years
    I've actually opened a bug back in September (it appears in the comments, I'll add it to the question itself now)
  • Ben Baron
    Ben Baron almost 8 years
    I'm running into a bug with this on the latest Swift (2.2 I think). The swift catch block always fires. However if there's not actually an exception, the error prints as "Foundation._GenericObjCError.NilError". Looking into a solution now.
  • Ben Baron
    Ben Baron almost 8 years
    Figured it out! You need to return something after tryBlock() runs successfully or it will always hit the catch block. I double checked that it still correctly fires on a real exception. I'll edit your answer to include this change.
  • Vitalii
    Vitalii over 7 years
    Works like a charm. Thanks!
  • valheru
    valheru over 7 years
    Wow is this still the only way to catch an NSException in Swift 2.x? Is that changed in 3.0?
  • Ssswift
    Ssswift about 7 years
    For STC, I'd recommend just copying the files into your project. I've run into way too many problems with trying to import and update STC via package managers. There's about 173 different forks of it. They support Swift 1, Swift 2, and/or Swift 3. They support Carthage, CocoaPods, and/or SPM. They support iOS, macOS, tvOS, and/or watchOS. But no one fork supports the particular combination of things I need (or has been updated in a year), and I wasted way too much time looking. Pick one, copy it into your project, and get on to more useful work.
  • matt
    matt almost 7 years
    It didn't work for me until I added an explicit return in the @catch block; edited to do that (and also removed the unnecessary let error, since the error always arrives into the general catch block as error)
  • tgaul
    tgaul over 6 years
    FWIW, in Swift 3, you can also add __attribute__((noescape)) to the type of tryBlock so that you can call methods on self when using it without needing to include self explicitly. This makes the full declaration + (BOOL)catchException:(__attribute__((noescape)) void(^)())tryBlock error:(__autoreleasing NSError **)error;.
  • Kartick Vaddadi
    Kartick Vaddadi over 6 years
    Great idea, upvoted. I made a couple of minor enhancements: 1) Return the exception to Swift as the return value of catchException(), so that it can check the exception class, or any properties like reason, including custom properties in an NSException subclass. Don't convert it to an NSError. 2) Catch id, in case someone throws an exception that doesn't derive from NSException.
  • VaporwareWolf
    VaporwareWolf over 6 years
    This has solved my problem. Thanks much. I'm using J2Obj-C in my project. The object throws java exceptions which are presented as NSExceptions. I couldn't catch them in Swift until I adapted the above (using Swift 3).
  • Giru Bhai
    Giru Bhai about 6 years
    After adding it breakPoint hit to try block not in the catch block,i.e. catchblock not firing, please help.
  • jk7
    jk7 over 4 years
    In my experience, the calls to objc code need to be directly within the tryBlock. If the tryBlock calls a Swift function which in turn calls objc code, the NSException is unhandled leading to a SIGABRT.
  • jk7
    jk7 over 4 years
    @VaddadiKartick It would be great if you could post a separate answer showing how you returned an NSException (or id) instead of an NSError, including how the ObjC.catchException call looks.
  • Rand
    Rand over 4 years
    @jk7 here is an example of building an NSError from and NSException stackoverflow.com/a/43561618/2864316
  • Justin Ganzer
    Justin Ganzer about 3 years
    This and any of the other solutions will result in memory leaks depending on the content of the tryBlock, should an exception occur. Does anyone have a solution to this?