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.