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

Pitch: guard try with separate else and catch blocks


Motivation

guard is the idiomatic way to exit early on failure and keep the success path unindented. But when the condition involves a throwing expression, the two most common workarounds both have problems:

try? loses the error:

guard let payload = try? JSONEncoder().encode(something) else { return }

If encoding fails you have no idea why. The thrown error is gone.

do/catch inverts the structure:

let payload: Data
do {
    payload = try JSONEncoder().encode(something)
} catch {
    logger.error("Failed to encode: \(error)")
    return
}

This works, but it fights guard's intent. The happy path is now buried inside a do block, and the early-exit logic is split from the declaration.

Proposal

Extend guard to support a catch block alongside the existing else block. The two blocks serve distinct purposes:

  • else — the expression succeeded but the condition was not met (e.g. result was nil, or returned false)
  • catch — the expression threw an error
guard let data = try fileHandle.read(upToCount: 1024)
else { return }
catch { logger.error("\(error)"); return }

Both blocks must exit the current scope, same as today's else. Each is independent and optional — you only write the blocks relevant to your expression.

All combinations

1. Non-throwing optional — existing syntax, unchanged:

guard let x = expr else {
    // x was nil
    return
}

2. Throwing, non-optionalcatch only, no else:

guard try validate(something)
catch {
    logger.error("Validation failed: \(error)")
    return
}

3. Throwing, optional — both blocks:

guard let data = try fileHandle.read(upToCount: 1024)
else {
    logger.warning("End of file reached")
    return
}
catch {
    logger.error("Read failed: \(error)")
    return
}

4. Multiple catch clauses (matching typed errors, just like do/catch):

guard let data = try fileHandle.read(upToCount: 1024)
else { return }
catch is CancellationError { throw CancellationError() }
catch { logger.error("\(error)"); return }

Semantics

This is syntactic sugar over do/catch combined with an optional-binding guard:

guard let data = try fileHandle.read(upToCount: 1024)
else { <else-body> }
catch { <catch-body> }

// desugars to:
let data: Data
do {
    guard let _result = try fileHandle.read(upToCount: 1024) else {
        <else-body>
    }
    data = _result
} catch {
    <catch-body>
}

The error binding in catch follows the exact same rules as in a do/catch block — implicit any Error unless a typed pattern is used.

Why separate else and catch?

Because nil and a thrown error are different failure modes that often warrant different responses. Merging them into a single else block (as the earlier version of this pitch proposed) forces you to distinguish them manually — or ignore one of them. Keeping the blocks separate makes the intent explicit and the code self-documenting.

Why not just use do/catch?

You can — and today you must. But guard exists to keep failure handling out of the way of the success path. A throwing expression that also produces an optional is a real and common case (file I/O, network reads, any throws API returning T?). Today there is no good way to handle both failure modes at the guard site without restructuring the surrounding code significantly.

Impact

Source-compatible addition. No existing guard syntax changes meaning. The feature composes with all existing guard forms: multiple conditions, where clauses, and pattern matching.

17 Likes

I would probably make it guard … catch {} rather than guard … else and make the error variable available.

It could probably even support multiple catch clauses, just ensuring each branch returns!

I think that would be very cool and elegant, I’d definitely use it!

5 Likes

having access to the error value is the key thing, otherwise we could just use try? But what I actually like in how you write it:
when the expression returns an optional, it is followed by else {} block,
and the catching could really be done in one or more catch {} blocks when it is throwing.
So we could have:

guard let data = try fileHandle.read(upToCount:1024)
else { /* in case of nil*/ }
catch { /* in case of exception */ }
2 Likes

The optional is caused by try? replacing thrown errors by a nil value so logically you shouldn’t even have to handle a nil case, unless of course the function returns an optional.

In either case I like the way you put it!

1 Like

FileHandle.read(uptocount:) is one example in the Foundation that can either return an optional, or throw. So even with plain try there are such situations. When using try? the whole expression is already not throwing any more, and we have only the else { } part.

1 Like

I think if this is allowed it should be a do-guard statement, not a guard-try. What I mean is that we take this existing thing:

do {
  guard let data = try fileHandle.read(upToCount: 1024)
  else { <else-body> }
} catch { <catch-body> }

and allow removing the brace around the do block, producing this shortened form:

do guard let data = try fileHandle.read(upToCount: 1024)
else { <else-body> }
catch { <catch-body> }

It’s a bit like not having to write an else if as else { if ... }; you’re allowed to omit the else's brace for convenience in this particular case.

Having a do here also lets us write do throws(MyError) guard … when we want to use typed throws, which would not work if the do was implicit.


Edit: on further thought, perhaps throwing from inside the else when using the braceless-do form should not produce the same thing as when enclosed in the do with braces, specifically the else-body throwing something should not be caught.

3 Likes

Re: that – yes, that's an issue. This pitch aside, if Swift could help by logging the error of try? that could solve probably 90% of the issue pressure if not more..


I like the pitch. Could be naturally extended to these as well:

// if-catch or if-else-catch
if let data = try fileHandle.read(upToCount:1024) {
    <if-block>
} else {
    <else-body>
} catch {
    <catch-body>
}
// try-catch (without do)
try fileHandle.read(upToCount:1024)
catch {
    <catch-body>
}

I like that.. if we have do there - then we don't need to extend guard/etc - it's still one do statement, just braceless.

This is not obvious... why?

3 Likes

I'd like to see a better description of how this feature would behave in case of throwing but non-optional, non-Void returning expressions.

Could guard let x = try expr catch { ... } be valid even if try expr evaluated as a non-optional value, say String?

I think throwing, optional-returning functions are quite rare in practice, and a feature like this wouldn't carry its weight if it only supported those.

1 Like

I think that would be possible with guard case let, in the same way if case let can with non-optional expressions.

Oh that's right! So this proposal would allow converting from:

let result: String
do {
  result = try compute()
} catch {
  return
}

to:

guard case let result = try compute() catch {
  return
}

Not so bad, I guess. I think the community would start seeing a lot more of case let variable = ... in these cases, which so far has been pretty obscure without any more specific pattern match destructures.

Besides, on the first read, the pitch made it sound like guard try expression could become a lot more common. But I think the existing language rules would allow the following only if validate returned a Bool:

guard try validate(something) catch { ... }

Otherwise (if -> Void), one would need to write it as:

guard case () = try validate(something) catch { ... }

I initially failed to consider the case where <else-body> could throw, and unless we want throwing in the else to be caught by the following catch (which seems undesirable to me), we can’t say it’s equivalent to the braced do {} catch version like I said initially.

I wonder if it would make more sense to force the catch to come before the else, like this:

do guard let data = try fileHandle.read(upToCount: 1024)
catch { <catch-body> }
else { <else-body> }

This way there’s no way to misinterpret the catch as catching what’s thrown in the else block simply handles anything thrown between the do and catch. It also makes more logical sense to order them like that because a failure by throwing is going to occur earlier than a failure when the guard condition evaluates to false.

4 Likes

I still don't see why: If we catch throws in the guard (if) part then why don't we want catching them in the else part – these two parts are equal peers to me.

I actually like what you said before about having "do" word there, just make an option to have it braceless... that way it works for any expression, without a need for special treatment... For example:

do while expression {
    ... 
} catch {
    ...
}

or

do foobar() catch {
    ...
}

and similarly for pretty much anything else including guard. I particular like the simplicity of this change both conceptual and hopefully implementational – just make the do braces optional..

3 Likes

To be clear, I don’t think we want to catch errors thrown in the else. But I also think easy to misinterpret the syntax as such. We have a do followed by a catch and only some parts of what’s in between is covered by the do-catch.

Maybe you see the else as a peer (like multiple catch at the same level are peers to each other), and I think it’s the pragmatic choice too, but I think interpreting the catch as applying to the block of code just above (that isn’t itself a catch) it is a valid to read it too. So it’s confusing. Hence why I think it’s better if the catch comes before the else.

Actually, I’d also change my suggestion above and move the do inside the condition to make it even clearer that the do-catch is solely applied to the condition:

guard do let x = try getOptionalX() catch {
  ... // error thrown, must exit scope
} else {
  ... // failed optional binding or condition, must also exit scope
}

And I’d make it work without the else for non-optional bindings:

guard do let x = try getX() catch {
  ... // error thrown, must exit scope
}

And I’d allow the same without the guard when there’s no binding and you can continue after the catch (same as what you suggested):

do doSomething() catch {
  ... // error thrown, but can continue in scope
}

I think this covers all bases.

So will it only catch errors thrown in expression, or will it also catch those thrown by the loop body? Personally, I’d expect errors in the loop body to be caught here, because the catch follows the loop body and it’s hard to see the loop body as a peer.

3 Likes

What would happen to the try's there? Or will I need to nest another do {} catch {} in the else part?!

Sure. Here's my suggestion:

// Proposed                     Today's equivalent
                          |
do while try foo() {      |     do { while try foo() {
    try bar()             |         try bar()
} catch {                 |     }} catch {
    // all caught here              // all caught here
}                         |     }
                          |
do try foo() catch {      |     do { try foo() } catch {
    // caught here                  // caught here
}                         |     }
                          |
do if try foo() {         |     do { if try foo() {
    try bar()             |         try bar()
} else {                  |     } else {
    try baz()             |         try baz()
} catch {                 |     }} catch {
    // all caught here              // all caught here
}                         |     }
                          |
do guard try foo() else { |     do { guard try foo() else {
    try bar()             |         try bar()
    return                |         return
} catch {                 |     }} catch {
    // all caught here              // all caught here
}                         |     }

These is how I envision the examples brought above should look:

do guard let data = try fileHandle.read(upToCount: 1024) else {
    return
} catch {
    logger.error("\(error)")
    return
}
let payload = do try JSONEncoder().encode(something) catch {
    logger.error("Failed to encode: \(error)")
    return
}

IOW - almost as today... just without braces for the do expression (plus today we don't have do expressions yet (which was a future direction of SE-0380.

3 Likes

I think a typical use of guard-else is to exit the scope by throwing. If catch catches those errors thrown by the else, then it’s confusing. For instance:

do guard let data = try fileHandle.read(upToCount: 1024) else {
    throw MyError.missingData
} catch {
    throw MyError.unableToReadData(underlyingError: error)
}

Should the MyError.missingData be caught by the line just below? If yes, how do you write this correctly then?

Whereas if the else comes after, then nobody will have to wonder if the catch will absorb the error thrown by the else:

guard do let data = try fileHandle.read(upToCount: 1024) catch {
    throw MyError.unableToReadData(underlyingError: error)
} else {
    throw MyError.missingData
}
5 Likes

At least this is exactly how it works today.. (if you put do expression in braces)

Well, my idea is that this guard-do-catch-else thing could make easier to express this pattern for handling optional binding and thrown error together :

let optionalData: Data?
do {
    optionalData = try fileHandle.read(upToCount: 1024)
} catch {
    throw MyError.unableToReadData(underlyingError: error)
}
guard let data = optionalData else {
    throw MyError.missingData
}

Being able to express this clearly would bring more value the language than if the else was forced inside of the virtual do block.

2 Likes

Got you, however I'm not sure your version is better compared to quite ergonomic:

    let optionalData = do try fileHandle.read(upToCount: 1024) catch {
        throw MyError.unableToReadData(underlyingError: error)
    }
    let data = try optionalData.unwrapOrThrow(MyError.missingData) // throws on `none`
1 Like

I want to point out that the 4th of your examples "todays equivalent" is not working.

do { guard try foo() else { // Cannot convert value of type '()' to expected condition type 'Bool'
    try bar()
    return
}} catch {
    // all caught here
}

that was part of the motivation starting this pitch. besides it would be nice with fewer scopes.
I like your comments and think that a simplified do <expression> catch {} is also a good syntactical companion.

Some thought about the scope of catching. It could by the order of the else and catch blocks. Basically to catch everything between the try and the catch block.
If the else block is in the front, it is also caught, if it is in the end, would t

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

In case we would like the scopeless do <expression> catch {} it could also work if it became an expression as well (like if, and switch from 5.9).