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:
- The
doblock needs to wrap the whole method (due to the scope oftodoItem). This means that the happy-path logic is indented (which is something thatguardspecifically helps to avoid); but additionally, it means that any errors from that logic will be caught by thecatchblock, perhaps accidentally. I'd much prefer thedoblock to only be scoped around theguard. - 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?