How to Catch an Exception in Swift

How to handle errors/exceptions in swift

The Swift do-try-catch syntax is designed to handle the errors analogous to the NSError patterns in Objective-C, not for handling fatal errors/exceptions. Do not be confused by the similarity of Swift's error handling do-try-catch with the completely different Objective-C exception handling pattern of @try-@catch.

One should only use forced unwrapping (!) and forced type casting (as!) when one knows with certainty that they cannot possibly fail. If they could fail (as in this case), you should gracefully detect this scenario and handle it accordingly.

You could, for example, use Swift error handling to communicate a failure converting a string to a date (and then use do-try-catch pattern when you call it in order to detect and handle that error):

enum DateError: Error {
case badDate
case dateNotFound
}

func getDateFromTextfield() throws -> Date {
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)

guard let dateString = dictOfLabelsAndText["Birthdate"] as? String else {
throw DateError.dateNotFound
}

guard let date = formatter.date(from: dateString) else {
throw DateError.badDate
}

return date
}

Then you could do:

do {
child.birthdateAT = try getDateFromTextfield()
} catch {
child.birthdateAT = formatter.date(from: "01.01.2000")
}

Or, more concisely, use try? and nil coalescing operator:

child.birthdateAT = try? getDateFromTextfield() ?? formatter.date(from: "01.01.2000")

Alternatively, you might just change the method to return an optional and use nil as a way of detecting a failure:


func getDateFromTextfield() -> Date? {
let formatter = DateFormatter()
formatter.dateFormat = "dd.MM.yyyy"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)

guard let dateString = dictOfLabelsAndText["Birthdate"] as? String else {
return nil
}

return formatter.date(from: dateString)
}

And then do:

if let birthDate = getDateFromTextfield() {
child.birthdateAT = birthDate
} else {
child.birthdateAT = formatter.date(from: "01.01.2000")
}

Or, again, use the nil coalescing operator:

child.birthdateAT = getDateFromTextfield() ?? formatter.date(from: "01.01.2000")

But, bottom line, do not use ! to unwrap an optional unless you know it can never be nil. Otherwise, use optional binding and/or guard statements to gracefully detect and report the failure.


This has been updated for contemporary versions of Swift. For original Swift 2 answer, see previous revision of this answer.

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

How to catch exception from non-throwing functions?

You can not use error handling for a function that doesn't throw so any combination of try or do/catch will not work

What you can do in this case is to check if the function returns a valid number like

let value = pow(3, 1000)

if value.isNaN {
//throw error or other error handling
}

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;
}

Swift do-try-catch syntax

There are two important points to the Swift 2 error handling model: exhaustiveness and resiliency. Together, they boil down to your do/catch statement needing to catch every possible error, not just the ones you know you can throw.

Notice that you don't declare what types of errors a function can throw, only whether it throws at all. It's a zero-one-infinity sort of problem: as someone defining a function for others (including your future self) to use, you don't want to have to make every client of your function adapt to every change in the implementation of your function, including what errors it can throw. You want code that calls your function to be resilient to such change.

Because your function can't say what kind of errors it throws (or might throw in the future), the catch blocks that catch it errors don't know what types of errors it might throw. So, in addition to handling the error types you know about, you need to handle the ones you don't with a universal catch statement -- that way if your function changes the set of errors it throws in the future, callers will still catch its errors.

do {
let sandwich = try makeMeSandwich(kitchen)
print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
print("Not me error")
} catch SandwichError.DoItYourself {
print("do it error")
} catch let error {
print(error.localizedDescription)
}

But let's not stop there. Think about this resilience idea some more. The way you've designed your sandwich, you have to describe errors in every place where you use them. That means that whenever you change the set of error cases, you have to change every place that uses them... not very fun.

The idea behind defining your own error types is to let you centralize things like that. You could define a description method for your errors:

extension SandwichError: CustomStringConvertible {
var description: String {
switch self {
case NotMe: return "Not me error"
case DoItYourself: return "Try sudo"
}
}
}

And then your error handling code can ask your error type to describe itself -- now every place where you handle errors can use the same code, and handle possible future error cases, too.

do {
let sandwich = try makeMeSandwich(kitchen)
print("i eat it \(sandwich)")
} catch let error as SandwichError {
print(error.description)
} catch {
print("i dunno")
}

This also paves the way for error types (or extensions on them) to support other ways of reporting errors -- for example, you could have an extension on your error type that knows how to present a UIAlertController for reporting the error to an iOS user.

Why the swift throws exception despite try?

try? does not catch exceptions. It catches thrown errors. Those are different things in Swift. Exceptions are at the Objective-C level and cannot be caught by Swift at all (they can't be safely caught in ObjC in most cases either, but that's a different discussion).

The solution in this case is to use JSONEncoder rather than JSONSerialization. JSONEncoder is a pure-Swift system. JSONSerialization is bridged from ObjC.

let body = try? JSONEncoder().encode([data])

See Handling Errors for more information:

Error handling in Swift resembles exception handling in other languages, with the use of the try, catch and throw keywords. Unlike exception handling in many languages—including Objective-C—error handling in Swift does not involve unwinding the call stack, a process that can be computationally expensive. As such, the performance characteristics of a throw statement are comparable to those of a return statement.

If you want to use JSONSerialization, it's important to recognize that it is a programming error to call it this way. The exception is intended to crash the program (even in ObjC). The correct way to write this code is:

if JSONSerialization.isValidJSONObject([data]), // <=== first, check it is valid
let body = try? JSONSerialization.data(withJSONObject: [data]) {
print("success")
} else {
print("unable to make body for call")
}

See the docs for more information:

If obj will not produce valid JSON, an exception is thrown. This exception is thrown prior to parsing and represents a programming error, not an internal error. You should check whether the input will produce valid JSON before calling this method by using isValidJSONObject(_:).

The thrown error from JSONSerialization is only to indicate an internal error in the serializer, not an attempt to encode an invalid object:

error

If an internal error occurs, upon return contains an NSError object with code NSPropertyListWriteInvalidError that describes the problem.



Related Topics



Leave a reply



Submit