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


(Jon Hull) #1

+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>> 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.