Pitch: `guard try` — error-aware early exit

interestingly this even works, only the guard is not aware of the throwing.

do {
    guard case () = try foo() else {
        // 'guard' condition is always true, body is unreachable
    }
} catch { }

I meant func foo() throws -> Bool there.

Is there any use case for that though? To me it sounds like a trap people would fall into if allowed, and it’s on the error path which tend to be less well tested.

But lets say we allow it, can we interleave both?

guard let  ... = try <expression>
catch { } // Catches only from <expression>
else { throw ElseError }
catch { } // Catches ElseError

How about a new guard do syntax:

guard do {
  try anything()
} catch {
  // must stop executing the enclosing scope, eg.
  return xxx // OK
  throw error // OK
}

This syntax will be more composable in my opinion.

I wonder if we can avoid introducing new special case syntax by making do…catch an expression? Then you could combine it with guard like any other expression. It would look like this:

guard let x = (do { try f() } catch { log(error); nil }) else { return }

You'll be glad to know that there are 949 posts (now 950) on that exact topic!

2 Likes

Overall allowing for a block from a guard clause, besides the criterion, to potentially enter into a block with the same context would break the expectation that guard is meant to prevent invalid, irreconcilable or undesired execution states from reaching a potential downstream input into a fragile or critical execution path and safely departing with an appropriate context departure state or resulting in further execution being unable to occur via a Never function.

If a guard clause’s exit blocks ever returns to the context the guard clause originated from then something has gone drastically wrong during execution and compilation.

If you do want to catch errors from a guard clause’s blocks; my advice is to make a new context with do, or catch the errors within the block before it exits the context. Doing this is more semantically clear and communicates better the intent of the result needs to be caught instead of any positional requirements, if you are throwing errors to correct them in the same statement but a different block you should reconsider the structure of the throwing block to have the catch clause be contained in the throwing block rather than a different block.

Making it easy to juggle how blocks connect with language features, like guard having the capacity to catch from the blocks which are intended to be safe exit paths, would also go against the Idea that Swift should make it easier to write clear and correct code.

TLDR:
Catching from a guard clause’s blocks from within the same context is fully incongruent with the conceptual expectations of guard in Swift’s syntax regardless of if they are both contained in the same clause or statement.

1 Like