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 wasnil, or returnedfalse)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-optional — catch 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.