Swift 2 Error Handling and While

Swift Error handling best practices

You must handle your exceptions by throwing errors and guard and checking if else everything.

This is an assumption that doesn't have to be true. If your code is structured correctly, you shouldn't have to check everything with if lets and throws.


I need general Swift error handling tips and best practices ...

Before you look at anything else, read the following pages in order. They should give you a good background on best practices.

  1. Error Protocol Apple Developer Documentation
  2. Error Handling - Apple's Swift Programming Language Guide
  3. Magical Error Handling in Swift - Ray Wenderlich

Im using Alamofire for calling services, which has closure, and calling it through another function with closure too. AFAIK I can’t throw error inside async code, so what is best practice for such case?

You can. A good practice is to pass a closure as a parameter to your service-calling function, then call the closure when the asynchronous operation is complete, like so:

functionThatCallsAService(completion: @escaping (Data?, NetworkingErrors?) -> ()) {
session.dataTask(with: request) { data, response, error in
guard error == nil else {
completion(nil, NetworkingErrors.returnedError(error!))
return
}
completion(data, nil)
}.resume()
}

enum NetworkingErrors: Error {
case errorParsingJSON
case noInternetConnection
case dataReturnedNil
case returnedError(Error)
case invalidStatusCode(Int)
case customError(String)
}

Is it favourable to have every function in the app throw errors? Just in case? Like checking every single value or result.

If you know for sure that a function or value won't be nil/cause a runtime error/throw an error, then don't check for it! But generally, according to the web pages above, you should check for nil and/or errors from calling a web service, interacting with the file system, creating a complex object, etc.


Can I have a Singleton Error handling module?

You technically can, but I don't see any reason to do so besides logging.

Swift error handling, what are the errors though?

For methods in the Apple frameworks look into the documentation and compare the method signature with its Objective-C equivalent.

In this specific case the Objective-C equivalent is

+ (id)JSONObjectWithData:(NSData *)data
options:(NSJSONReadingOptions)opt
error:(NSError * _Nullable *)error

so the object in the catch statement is an NSError object

do {
let jsonData = try JSONObjectWithData(someData, options:NSJSONReadingOptions())
} catch let error as NSError {
print(error)
}

Swift 2.2 error handling by try block

You can catch all errors individually with do/catch:

do {
try requestBeverage(-1, coins: 4)
} catch VendingMachineError.InvalidSelection {
print("Invalid selection")
} catch VendingMachineError.OutOfStock {
print("Out of stock")
} catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
print("You need \(coinsNeeded) more coins")
} catch {
// an unknown error occured
}

print("finished...")

Alternatively, use try? if you only care about whether an error is thrown, but not which one:

func requestSomeBeverage() {
guard (try? requestBeverage(-1, coins: 4)) != nil else {
print("An error has occured")
return
}
}

requestSomeBeverage()
print("finished...")

If you're absolutely sure an error will not be thrown, and you want to cause an exception when it does, use try! (but in most cases, don't):

try! requestBeverage(-1, coins: 4)
print("finished...")

Swift 2.0 exception handling

As @JeremyP said, in swift there is no exceptions. To make this clear you can you can use a single do-catch block to catch multiple errors, nevertheless if-let and guard has to be used to avoid fatal errors which are not catchable. For example:

do {
try method1()
try method2()
} catch {
// do something with the error
}

but the previous example won't handle a fatal error, for example:

do {
try method1()
let myVar = anOptionalVar!
} catch {

}

this code won't handle a fatal error, if anOptionalVar is nil and you force unwrap it this will cause a fatal error which will terminate execution.

So the pattern described by @JeremyP should be used to handle errors:

enum myError: ErrorType {
case BadError
}

do {
guard let myVar = anOptionalVar else { throw MyError.BadError }
} catch MyError.BadError {
// do something with the error
}

So as of swift 2 there is no way around using guard and if-let to stay safe from fatal errors, your only option is using them and propagate an ErrorType instead.

Swift error handling - determining the error type

You can catch the thrown error to a variable and do runtime analysis of the variable. E.g., for some unknown implementation:

/* ---------------------- */
/* unknown implementation */
enum HiddenError: ErrorType {
case SomeError
}

class AnotherError : NSError { }

func foo() throws -> Int {
let foo = arc4random_uniform(3);
if foo == 0 {
throw HiddenError.SomeError
}
else if foo == 1 {
throw AnotherError(domain: "foo", code: 0, userInfo: [:])
}
else if foo == 2 {
throw NSError(domain: "foo", code: 0, userInfo: [:])
}
else {
return Int(foo)
}
}
/* ---------------------- */

Investigate the error as:

/* "External" investigation */
func bar() throws -> Int {
return try foo()
}

func fuzz() {
do {
let buzz = try bar()
print("Success: got \(buzz)")
} catch let unknownError {
print("Error: \(unknownError)")
print("Error type: \(unknownError.dynamicType)")
if let dispStyle = Mirror(reflecting: unknownError).displayStyle {
print("Error type displaystyle: \(dispStyle)")
}
}
}

fuzz()
/* Output examples:

Error: SomeError
Error type: HiddenError
Error type displaystyle: Enum

//

Error: Error Domain=foo Code=0 "(null)"
Error type: AnotherError
Error type displaystyle: Class

//

Error: Error Domain=foo Code=0 "(null)"
Error type: NSError
Error type displaystyle: Class */

HTTP POST error Handling in Swift 2

You presumably want to wrap your NSJSONSerialization calls in do/try/catch logic as shown below.

In Swift 3:

var request = URLRequest(url: URL(string: urlString)!)

let session = URLSession.shared
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")

request.httpBody = try! JSONSerialization.data(withJSONObject: parameters)

// or if you think the conversion might actually fail (which is unlikely if you built `parameters` yourself)
//
// do {
// request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
// } catch {
// print(error)
// }

let task = session.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
print("error: \(error)")
return
}

// this, on the other hand, can quite easily fail if there's a server error, so you definitely
// want to wrap this in `do`-`try`-`catch`:

do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
let success = json["success"] as? Int // Okay, the `json` is here, let's get the value for 'success' out of it
print("Success: \(success)")
} else {
let jsonStr = String(data: data, encoding: .utf8) // No error thrown, but not dictionary
print("Error could not parse JSON: \(jsonStr)")
}
} catch let parseError {
print(parseError) // Log the error thrown by `JSONObjectWithData`
let jsonStr = String(data: data, encoding: .utf8)
print("Error could not parse JSON: '\(jsonStr)'")
}
}

task.resume()

Or, in Swift 2

let request = NSMutableURLRequest(URL: NSURL(string: urlString)!)

let session = NSURLSession.sharedSession()
request.HTTPMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")

request.HTTPBody = try! NSJSONSerialization.dataWithJSONObject(parameters, options: [])

// or if you think the conversion might actually fail (which is unlikely if you built `parameters` yourself)
//
// do {
// request.HTTPBody = try NSJSONSerialization.dataWithJSONObject(params, options: [])
// } catch {
// print(error)
// }

let task = session.dataTaskWithRequest(request) { data, response, error in
guard let data = data where error == nil else {
print("error: \(error)")
return
}

// this, on the other hand, can quite easily fail if there's a server error, so you definitely
// want to wrap this in `do`-`try`-`catch`:

do {
if let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? NSDictionary {
let success = json["success"] as? Int // Okay, the `json` is here, let's get the value for 'success' out of it
print("Success: \(success)")
} else {
let jsonStr = String(data: data, encoding: NSUTF8StringEncoding) // No error thrown, but not NSDictionary
print("Error could not parse JSON: \(jsonStr)")
}
} catch let parseError {
print(parseError) // Log the error thrown by `JSONObjectWithData`
let jsonStr = String(data: data, encoding: NSUTF8StringEncoding)
print("Error could not parse JSON: '\(jsonStr)'")
}
}

task.resume()

I'd also suggest being a little more careful about forced unwrapping of data, because you want to detect/handle the errors, not crash. For example, above I use a guard statement to unwrap it.

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.

Best practice for Swift methods that can return or error

To add an answer to this question (five years later), there’s a dedicated Result type for this exact scenario. It can return the type you want on success, or type an error on failure.

It does mean re-factoring some code to instead accept a completion handler, and then enumerating over the result in that callback:

class SecurityService {
static func loginWith(email: String, password: String, completionHandler: @escaping (Result<User, SecurityError>) -> Void) {
// Body
}
}

Then in a handler:

securityService.loginWith(email: email, password: password) { result in
switch result {
case .success(let user):
// Do something with user
print("Authenticated as \(user.name)")
case .failure(let error):
// Do something with error
print(error.localizedDescription)
}
}


Related Topics



Leave a reply



Submit