Andrzej Doyle
Andrzej Doyle

Reputation: 103847

Handling exceptions in guard blocks

I'm looking for an elegant way of combining guard blocks with do-try-catch semantics in Swift, and haven't been satisfied with my own efforts so far.

For background, I'm getting hold of some necessary data from a function that throws an exception and can return an Optional value. As a concrete example, let's say I'm looking up the most recent item from a TODO list stored on the filesystem. This can legitimately be nil (nothing on the list), or it can thrown an exception (some sort of I/O error accessing the filesystem), so both cases are distinct.

For the rest of the method though, we only care if there is a item to operate on - so this sounds like an ideal case for guard:

func myCoolMethod() {
    guard let todoItem = try? fetchItemFromList() else {
        // No item to act on, nothing to do
        return
    }

    // rest of function acts on todoItem
    ...
}

This is indeed fine - until the guard block starts failing when you expect it to succeed, and you'd really like to (if nothing else) log the error message to try and investigate. As such, the try? needs to be replaced by a try to allow the error to be captured, and now we'll need to wrap the guard in a do block:

func myCoolMethod() {
    do {
        guard let todoItem = try fetchItemFromList() else {
            // No item to act on, nothing to do
            return
        }

        // rest of function acts on todoItem
        ...
    }
    catch {
        // Now we know there was an error of some sort (can specialize the
        // catch block if we need to be more precise)

        // log error or similar, then...
        return
    }
}

There are two problems with this that I see:

  1. The do block needs to wrap the whole method (due to the scope of todoItem). This means that the happy-path logic is indented (which is something that guard specifically helps to avoid); but additionally, it means that any errors from that logic will be caught by the catch block, perhaps accidentally. I'd much prefer the do block to only be scoped around the guard.
  2. There's duplication of the "what to do if we have no item" block. In this trivial example it's just a return, but it still pains me a little to have two separate blocks that need to do broadly the same thing (where one just adds a little extra context with the error being available).

I'd like to get as close to this as possible:

func myCoolMethod() {
    // `do guard` is made up but would enter the else block either if an exception
    // is thrown, or the pattern match/conditional fails
    do guard let todoItem = try fetchItemFromList() else {
        if let error = error {
            // Optional `error` is only bound if the guard failed due to an exception
            // here we can log it etc.
        }

        // This part is common for both the case where an exception was thrown,
        // and the case where the method successfully returned `nil`, so we can
        // implement the common logic for handling "nothing to act on"
        return
    }

    // rest of function acts on todoItem
    ...
}

What are my options here?

Upvotes: 2

Views: 864

Answers (1)

Rob Napier
Rob Napier

Reputation: 299643

First, look for errors and deal with them. Then look for existence, and deal with that:

func myCoolMethod() {
    // Making this `let` to be stricter. `var` would remove need for `= nil` later
    let todoItem: Item?

    // First check for errors
    do {
        todoItem = try fetchItemFromList()
    } catch {
        todoItem = nil  // Need to set this in either case
        // Error handling
    }

    // And then check for values
    guard let todoItem = todoItem else {
        // No item to act on (missing or error), nothing to do
        return
    }

    // rest of function acts on todoItem
    print(todoItem)
}

Or separate the problems into functions. When things get complicated, make more focused functions:

func myCoolMethod() {
    // And then check for values
    guard let todoItem = fetchItemAndHandleErrors() else {
        // No item to act on (missing or error), nothing to do
        return
    }

    // rest of function acts on todoItem
    print(todoItem)
}

func fetchItemAndHandleErrors() -> Item? {
    // First check for errors
    do {
        return try fetchItemFromList()
    } catch {
        // Error handling
        return nil
    }

}

Upvotes: 2

Related Questions