Swift Error Handling Through Try Variance

Error Handling through try variance



Each and every system has erroneous state, smart system handle those error and fall gracefully, so that the end user don’t just freakout. Swift also provides us with some cool error handling system which can be really worthy when used with try and enum.

On this short blog post we will talk about Swift error handling mechanism. We will concentrate on try to handle those error. Also the reason for choosing enum as the first choice for defining error model.

Background

We already have some talk over enum. On the enum hub page we can find the details on enum arena.

Pretalk on Error handling

So why do we want Error handling? Well error can happens at any stage even on the most well defined and well architectural app. We have to move on by understanding one true thing, error will occur. How will we handle that error is the important part. Because how would you like an app, which crashes often or regular? No one likes that.

We used to spend some good weeks or even months to build up a moderated app. Just putting some more hours on error handling would make that app a good one. User will get a smooth feelings with the app. Even on the most erroneous situation the app does not crash, this gets the trust from the valuable user. And we all know gaining user’s trust is a crucial part for the long run.

Enough talk, think we all understand the simple thing. Adding proper error handling won’t take long, but give a very high return. Worth investing?

Error

On Swift Error is an empty protocol, with no requirements to fill. So any type can adapt the Error protocol.

On Swift only an instance of Error can be thrown. So we need to adopt the Error protocol, if we want to use that type as an throwable when error occurs.

Thinks will be more clear on the next section.

enum as Error why?

A lot of times we will/had listen that, enum are perfect for Error conformation. But why? What is the relation thats makes enum suitable for Error.

We can always visits the enum blog series for more in-depth knowledge on enum.

Back to our original topic. As we know enum are pre defined case based architecture, and at a time only one value/case is true for an enum instance. This goes pretty much same for an Error. At a time only one error can occur among pre define Error set.

Also additional information for a specific error sometimes may be needed. As an example: for age limit verification failure there cloud be overaged or underaged Error. enum‘s associated value cloud serve this extra information. We will have an example on this issue later on this blog post.

throws and rethrows

Let us talk with the throws keyword first. When a function has throws keyword on the definition, that means the function can throw error and the function is called throwing function.

On the other side, when a function has rethrows keyword on the definition then that function is called rethrowing function. Now to be a rethrowing function that function, should have a functional parameter which is a throwable function. Let us have an example:

enum ReminderError: Error{
    case invalidParam
}

var divisor = 0

func hundredReminder() throws -> Int{
    if divisor == 0 {
        throw ReminderError.invalidParam
    }
    return 100 % divisor
}

func absReminder(input: Int, reminder: () throws -> Int) rethrows -> Int{
    divisor = input < 0 ? abs(input) : input
    return try reminder()
}

do {
    try absReminder(input: -0, reminder: hundredReminder)
} catch ReminderError.invalidParam {
    "Check ur input"
}

On the above example hundredReminder() is a throwing function as it has the throws keyword on the definition. And absReminder(input: Int, reminder: () throws -> Int) rethrows -> Int is a rethrowing function. Have a look at the reminder params which states that it is a throwing function, thats why the absReminder needed to be a rethrowing function.

If there is some confusion, don’t tense. I will be clear on this blog post. It is step by step learning. We just clear the throws and rethrows concepts. Now lets move to our next topic.

Error Handling

We already have some idea on how to handle errors, and those errors are recoverable error. So the thing is pretty straight forward. We will throw an error if something goes wrong and on the caller we will handle those error to prevent a disaster crash, graceful fall may be . So there are some ways we can handles the error. We will talk about those in the upcoming sections.

Propagation of Error

The concept of throwing an error is called propagating of Error or Error propagation. So each time a throwing function throws an error it is actually propagating the error. Simple ๐Ÿค—.

try with do-catch block

On this approach the throwing function is marked with try and is surrounded by a do-catch block. Example follows:

enum PrintError: Error{
    case InvalidName
    case EmptyName
}

func validate(name: String?) throws{
    guard let name = name else {
        throw PrintError.InvalidName
    }
    
    if name.isEmpty {
        throw PrintError.EmptyName
    }
}

func print(name: String?){
    do {
        try validate(name: name)
        print("Name is: \(name!)")
    } catch PrintError.InvalidName {
        print("Got an Invalid Name")
    } catch PrintError.EmptyName {
        print("Got an Empty Name")
    }catch {"Other error \(error.localizedDescription)"}
}

print(name: "") //Prints: Got an Empty Name
print(name: nil) //Prints: Got an Invalid Name
print(name: "Mobi Dev Talk") //Prints: Name is: Mobi Dev Talk

Here validate(name: String?) is the throwing function and on the print(name: String?) we are handling the error. Inside the do-catch block we are getting all the defined error of PrintError including a final catch case, "Other error \(error.localizedDescription)". So now the question is where did the error comes from? Well error is an instance of Error, more appropriately local error constant. This default error is provided by the Swift error handling system. If none of the above error-patterns matches then this final catch case with error will execute. Hmm smart move.

One crucial point: if we do not provide the final catch case ,with builtin error var, then there is a potential crash possibility of unhandled error. So we always should provide the final catch case.

Usage of try with do-catch block

  • When safety is the major concern.
  • When we need to know what specific type of error occurred.

Optional value through try?

Using try? will provide an optional value, so either there will be a proper value or there will be nil. The try? will not provide any error details, rather will provide nil if any error occurs.

func possiblePrint(name: String?) throws -> String{
    guard let name = name else {
        throw PrintError.InvalidName
    }
    
    if name.isEmpty {
        throw PrintError.EmptyName
    }
    
    return name
}

var possibleName = try? possiblePrint(name: "")
possibleName?.capitalized // nil

possibleName = try? possiblePrint(name: nil)
possibleName?.capitalized // nil

possibleName = try? possiblePrint(name: "mobi dev talk")
possibleName?.capitalized // Mobi Dev Talk

On the above example possiblePrint(name: String?) is a throwing function. It will returns a String if no error is thrown.

We declare possibleName as a var for reusability. Each time we are assigning possibleName we are using the try?. As a result possibleName is optional String, String?, even though possiblePrint(name: String?) returns a String instance not a String?. The try? is converting the proper value of String to optional string, String?.

Usage of try?

When we have no concern about the error. We are only interested on the operation or on the value, if the throwing function succeeded.

Disabling Error propagation through try!

This option is the most dangerous one. try! will disable any error propagation. So if any error occurs immediately the app will crash.

var unwrappedName = try! possiblePrint(name: "mobi dev talk")
unwrappedName.capitalized // Mobi Dev Talk

//unwrappedName = try! possiblePrint(name: "")
//unwrappedName.capitalized // crashs the app

//unwrappedName = try! possiblePrint(name: nil)
//unwrappedName.capitalized // crashs the app

The unwrappedName has a type of String as the try! will forcefully unwrapped the returned value, cloud be either String or an instance of Error.

For a valid name the app will not crash but for an empty or a nil value the app will surely crash ๐Ÿ’ฅ.

Usage of try!

๐Ÿ’ฃ When the developer wants to nuke the code base ๐Ÿ”ฅ

Result<T>

There is a common patterns to extract success or fail result. Very useful on asynchronous call. The syntax as follows:

enum Result<Value, Error: Swift.Error>{
    case success(Value)
    case fail(Error)
}

Here the Value is a generic type. We can use the above Result enum as our functional parameter to handle error when we are doing an asynchronous call. The target of this section is to have an introduction with the patterns. For more info we can visit Sundell’s blog which has a beautiful descriptive blog post covering this topic.

enum conforming Error protocol, with associated value

One last thing to talk about, before wrapping up this blog post. How associated value can help to provide the additional information for a specific error.

Say we have a registration workflow which has an age limitation, the limit is over 18 and under 30. We want to know why a registration request would fail? Is it because of an invalid username or is it because of an improper age of the user. If the reason is improper age then we would like to know, if the user is overaged or underaged.

So here goes the code:

enum AgeError : String{
    case underaged
    case overaged
}

enum RegistrationError : Error{
    case invalidUserName
    case improperAge(AgeError)
}

func registration(userName: String, age: Int) throws {
    if userName.isEmpty {
        throw RegistrationError.invalidUserName
    }
    
    if age < 18 {
        throw RegistrationError.improperAge(.underaged)
    }else if age > 30{
        throw RegistrationError.improperAge(.overaged)
    }
}

do {
    try registration(userName: "mobidevtalk", age: 0)
} catch RegistrationError.improperAge(let age) {
    age //underaged
}

do {
    try registration(userName: "mobidevtalk", age: 90)
} catch RegistrationError.improperAge(let age) {
    age //overaged
}

As we can see the improperAge of RegistrationError has an associated value. On current time it is an enum, AgeError, having a String raw type.

Here the associated value of improperAge is providing the additional information, underaged or overaged, when there is an age violation on the registration flow.

Best practices, becoming pro

  • Choose try with do-catch block as default error handler. Always provide the final catch case for handling the unspecified errors.
  • If and only if, you do not need the error then use the Optional value type through try?.
  • Are you expecting some advice to use try!? I have only one. Pray to God. God dammit !!! never ever use try! on production code.

So that is it for today. The source is used here are shared at GitHub.Our error handling with try variance is now completed. Happy Error Handling. ๐Ÿ––

3 thoughts on “Error Handling through try variance

Leave a Reply

Notifications for mobidevtalk! Cool ;) :( not cool