Swift Error Handling Go Way

I’ll start by explaining the recommended error handling in Apple Documents then I’ll continue why I prefer Go style error handling.

Error Handling Apple Way

You can have multiple types of error

enum MyCustomError: ErrorType {
case Bad
case Worse
}

To indicate that a function, method, or initializer can throw an error, you write the “throws” keyword in the function’s declaration after its parameters.

func canThrowErrors() throws -> String
func cannotThrowErrors() -> String

Example

func SomeFunction(someArg: String) throws -> String {
guard someArg != "Bad" else {
throw MyCustomError.Bad
}
guard someArg != "Worse" else {
throw MyCustomError.Worse
}

return "Success"
}
// Option 1 - Optional 
let result1 = try? SomeFunction(“Bad”) // result1 is “nil”
let result2 = try? SomeFunction(“Good”) // result2 is “Success”
// Option 2 - Force-Value
let result1 = try! SomeFunction(“Bad”) // runtime error
let result2 = try! SomeFunction(“Good”) // result2 is "Success"
// Option 3 - do-catch
do {
try SomeFunction(“Worse”)
} catch MyCustomError.Bad {
print(“Bad Error”)
} catch MyCustomError.Worse {
print(“Worse Error”) // Will print "Worse Error"
}

Error Handling Go Way

The Go team purposefully chose errors to be values.

Values can be programmed, and since errors are values, errors can be programmed. Errors are not like exceptions. There’s nothing special about them, whereas an unhandled exception can crash your program.

I recommend you to read https://blog.golang.org/errors-are-values.

A simple error handling in Go.

result, err := SomeFunction() 
if err != nil {
// handle the error
}

Go vs Swift

In Swift, errors are also represented by values which is also the main reason why Go choose errors to be values.

In Swift, errors are represented by values of types that conform to the ErrorType protocol.

In Go,

Errors are values. Values can be programmed, and since errors are values, errors can be programmed.

Even though both Go and Swift uses errors as values the main difference comes from Go’s simplicity which is reflected in the syntax of error handling.

In Go:

result, err := SomeFunction() 
if err != nil {
// handle the error
}
// happy path proceeds as normal without nesting

In swift:

do { 
// If successful, the happy path is now nested.
} catch (let error as NSError) {
// handle error
}

You can avoid nesting using “guard” keyword. But the down side of “guard” keyword is you can not catch which error occurred.

guard let result = try? SomeFunction() else {
// no way to catch which error occurred
return
}
// result is in scope, proceed with happy path

If you want to catch which error was thrown “do-catch” should be used.

Error Handling In Swift: The Go Way

We want to use errors as normal values. That’s why we should return the error value from the function instead of marking the function as “throws”. Also error types should be optional since their value could be nil.

func SomeFunction() -> MyCustomError?

If the function needs to return additional values we need to use tuples as the function's return value

func SomeFunction() -> (String, MyCustomError?)

Here is a full example

func SomeFunction(someArg: String) -> (String, MyCustomError?) {
guard someArg != "Bad" else {
return ("", MyCustomError.Bad)
}
guard someArg != "Worse" else {
return ("", MyCustomError.Worse)
}

return ("Success", nil)
}
// Option 1: Handle error with guard
let (result, err) = SomeFunction("Success")
guard err == nil else {
print(err)
return
}
// Option 2: Handle error with if case
let (result, err) = SomeFunction(“Bad”)
if err != nil {
print(err) // Will print "Optional(MyCustomError.Bad)"
}

Pros of handling errors the way I described:

  • Lesser nesting compared to “do-catch” keyword
  • Subjectively simpler

Cons:

  • “throws” keyword allows to throw multiple functions just by using “try” keyword (without do-catch or !/?). Go like error handling will force you to check every single error until it is handled somewhere. For details search “favoriteSnacks” in Apple documentation.

Conclusion

I’ve shown multiple ways of handling errors in Swift. Performance wise the difference is irrelevant. Weighing pros and cons, there is no clear winner. Mostly it is a preference. If you like less nesting and early exits in functions this could be a method you’d want to use.