[pitch] Make exceptions visible in guard's else block

+1 for the idea of extending guard to allow catches (which still have to exit scope) before the else statement:

guard let x = try foo() catch MyError.errorType {
  //Handle error & exit scope
} catch MyError.otherErrorType {
  //Handle error & exit scope
} else {
  //x was nil
}

I think that this will come in handy when we start dealing with Results, and as Haravikk mentioned, it lets us use the resulting variables in the surrounding scope (They aren't trapped in the do block).

Thanks,
Jon

···

> On 16 Nov 2016, at 11:06, Nick Keets via swift-evolution <swift-evolution at swift.org <https://lists.swift.org/mailman/listinfo/swift-evolution&gt;&gt; wrote:
>
> Hello all, I'm interested in a pattern to return failure, together with an error
> to explain why. I think that the "guard let x = x" discussion touched on the issue,
> but didn't really go in that direction.
>
> Right now, optional and boolean results work nicely with guards, but you only get
> sucess/failure back. For example:
>
> func foo() -> Int?
> func bar() -> Bool
> func baz(Int) -> Int?
>
> guard
> let a = foo(),
> bar(),
> let b = baz(a)
> else {
> // No information about what failed here
> print("Something failed")
> return
> }
>
> I see a lot of enum Result solutions being proposed, but they have the fundamental
> problem of not having access to the error inside guards. For example:
>
> enum Result<T> { case sucess(T), error(String) }
>
> func foo() -> Result<Int>
>
> guard case let .success(value) = foo() else {
> // Result is .error but we have no access to the error message here
> return
> }
>
> I think a solution to this problem could be to allow "guard let try" statements
> that make the error available inside the else statement. So you could write:
>
> func foo() throws -> Int
> func bar() -> Bool
> func baz(Int) throws -> Int
>
> guard
> let a = try foo(),
> bar(),
> let b = try baz(a)
> else {
> // `error` is available here like in a catch block
> print("Error: \(error.localizedDescription)")
> return
> }
>
> A potential weirdness of this solution is that it appears indistinguishable from
> "guard let try?" (already available) if you are not interested in the error.
>
> Thoughts?

I think it comes to a point where you may be expecting too much of guard, when this is really what a switch or try/catch is for.

One of the purposes of switch is exhaustive handling for enums, so that's the best way to handle them if you're interested in more than one case. Whereas try/catch is exactly what you want if you want to, well, catch an error.

However, I wouldn't mind if maybe we could just have an optional catch on guard statements, like so:

  guard let a = try foo(), baz(), let b = try baz(a)
    else { return; } // baz() returned false, or foo() or baz() returned nil
    catch(error) { print("Error: \(error.localizedDescription)"); return } // foo() or baz() threw an exception

This would still have the same requirements as a guard's else statement (must change flow with break, return, throw etc.) but is only triggered in the exception case. If your guard condition can *only* fail as a result of an exception then you can omit the else. For example, if you skip the call to bar(), and foo() and baz(a) can't return nil then the following would be valid:

  guard let a = try foo(), let b = baz(a) catch(error) { print("Error: \(error.localizedDescription)"); return }

The main benefit here being that you get the same kind of in-scope a and b variables, unlike a try/catch block where they are limited to their own scope only (or you have to declare them outside which is messy).

I don't see how the enum case could be usefully simplified though, as you'd need to have pattern matching of some kind to define the error condition you expect, in which case you might as well just use a switch anyway, whereas a guard/catch at least eliminates an annoyance of try/catch when dealing with something that only fails initially.

1 Like

I've just came up with the same idea. I'm surprised that there are no replies here. Can we discuss it more?

4 Likes

What if we write

let x: MyStruct

do {
  x = try foo()
} catch MyError.errorType {
  //Handle error & exit scope
} catch MyError.otherErrorType {
  //Handle error & exit scope
} else {
  // Handle unknown error
}

print(x) // x has valid value when we reach here

Can this pattern solve your problem?

Yes, that can work!

But maybe declare the variables, that escape the do block, inside the parenthesis after the do word, like this:

do(let x: MyStruct) {
  x = try foo()
} catch MyError.errorType {
  //Handle error & exit scope
} catch MyError.otherErrorType {
  //Handle error & exit scope
} else {
  // Handle unknown error
}

print(x) // x has valid value when we reach here

Just to make sure this variable is glued to the "do-catch".

How is it different from this?

let x: MyStruct

do {
  x = try foo
} catch MyError.errorType {
  // Handle error & exit scope 
} catch MyError.otherErrorType {
  // Handle error & exit scope
} catch {
  // Handle unknown error
}

print(x) // x has valid value when we reach here
2 Likes

In case you weren't aware, a do statement does not need a catch statement following it and can instead be used to scope access to variables:

do {
  let x: MyStruct
  do {
    x = try foo()
  } catch {
    // handle errors
  }
}

// x is out of scope here

Well, I'd rather want the opposite :) I want to have access to x outside the "do" block like it happens with guard.

Okay, I didn't realize that the syntax suggested by @Zhu_Shengqi is actually a working Swift example and not just a suggestion ._. So here is my playground example

enum MyError : Error {
    case invalidInput
}

func parseInt(_ value: String) throws -> Int  {
    if let x = Int(value) {
        return x
    } else {
        throw MyError.invalidInput
    }
}

func myAction(value: String) {
    
    let x: Int
    do {
        x = try parseInt(value)
    } catch MyError.invalidInput {
        print("Error catched")
        return
    } catch {
        print("Unknown error catched")
        return
    }

    print(x * 2)
}

myAction(value: "unknown")

And Swift is even smart enough to tell me if in catch I don't init x / exit scope!

2 Likes