Motivation
It has recently been reaffirmed in SE-0143: Typed throws that throwing is considered Swift's native/preferred way of handling errors.
One relatively minor drawback, but still a persistent drawback nonetheless, is that when thrown errors are handled rather than propagated, it always necessitates additional nesting.
It is widely considered best practice nowadays to minimize nesting to improve readability and ease of reasoning through control flow. Indeed, many Swift language features, such as guard
, aid in this goal of minimizing nesting. It seems unfortunate therefore for completely common-case error handling to always require further nesting.
Proposal
The proposal in this pitch is to add defer catch
syntax that is considered identically equal to surrounding the rest of the execution scope with do
, followed by the specified catch
clause(s).
For example:
func foo() {
do {
// the happy path of this entire
// function now has to be nested
} catch {
// render error
}
}
becomes
func foo() {
defer catch {
// render error
}
// happy path is now unindented, in a similar fashion to
// what we can typically accomplish for non-throwing code
}
Note that the ensuing catch
clause(s) can be just as rich as regular catch
clause(s), e.g.:
func foo() {
defer catch let catError as CatError {
// render cat error
} catch let dogError as DogError {
// render dog error
} catch {
// render other error
}
// happy path
}
Likely the main risk for confusion this could be argued to introduce is that to a certain degree, you need to see the code beneath to understand the implications of the catch
clause. This is the main reason for the adoption of the existing keyword defer
, which already denotes this concept. In normal usage of the word defer
, you similarly need to mentally process code further below to understand the full implications of what code the deferred code follows.
Differences from earlier pitches
Somewhat related forms of this idea appear to have come up once or twice before, but have typically been a subset of much broader proposals that have resultantly been more complex and harder to reason about.
It is considered an important design principle of this proposal that it be treated identically equal to the existing, well-understood pattern of a single do
block followed by catch
clause(s). To understand the exact implications of code following this new pattern, one need only mentally convert it to the old pattern in a 1:1 fashion, not understand any new paradigms.
Super-brief discussion of alternatives considered
guard catch
rather thandefer catch
- While it matches some of the intent of
guard
to keep the happy path unindented, the overall semantics are different enough (specifically thatguard
means code below will NOT be run) that it almost certainly leads to more confusion
- While it matches some of the intent of
- Allowing
catch
clauses after the function definition itself (see below) is elegant in many ways, but by virtue of being so alien to pre-existing patterns, risks leading to much confusion (or mis-parsing to even well-trained eyes)
func foo() {
// happy path
} catch {
// render error
}