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


(Nick Keets) #1

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?


(Haravikk) #2

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.

···

On 16 Nov 2016, at 11:06, Nick Keets via swift-evolution <swift-evolution@swift.org> 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?