Optional Throws in Swift
Hi all, I have been looking for a way to add some flexibility to error handling, and I'm interested in fielding feedback on this idea.
Thanks for your time,
Michael
Background
Motivation
There are many cases when jurisdiction of error handling is unclear. Developers may question whether to handle or propogate errors.
Choosing to throw errors provides the benefit that callers can flexibly handle problems.
Choosing to not throw errors provides the benefit of simplifying syntax to users (no do-try-catch
).
Existing Solution: Fatal Crash
When operating in this unclear territory, developers may choose to fatally crash, like array subscript getters myArray[1]
. Fatal errors are currently the best way to omit error-handling syntax for users. Developers may then opt-in to bounds checks with if-statements
if they so choose to prevent fatal crashes.
Choosing to fatally crash provides users with two options:
- Omit error handling syntax and risk a fatal crash
- create some bound checks to avoid a fatal crash
Shortcomings of Fatal Crash
Opting into bound checks using if-statements
is largely inconsistent with current error handling practices in Swift (if let
, guard try?
, rethrows
...).
However, forcing these functions to throw could be a controversial topic -- regarding these calls are ubiquitous, and demanding error catching would uproot many existing codebases.
A Middle Ground
Another solution here is to provide two implementations of the same function: one that throws errors, and a non-throwing function that fails.
"Optional throws" is my pitch to create a throwing function and a non-throwing function, shorthanded through a single function signature.
Optional throws will let a function throw errors and provide default error handling. The user of the function will opt-in to handle thrown errors, or let the function handle its own errors. This would provide a safer way to encapsulate fatalError()
calls, like in array subscript getters, and streamline error-handling syntax. Callers may opt to catch errors, or let the function fatally crash.
Optional Throws Syntax
Throwing functions in Swift look like this:
func willThrow() throws {
throw MyError.oops
}
Optional throws introduce a throws?
keyword in the function signature, indicating the function is elligible to throw errors. The ?
syntax in Swift is already well-defined to mark optional values.
With introducing guard-throw?
, functions can define default error handling. The guard-throw?
syntax should be interpreted as:
"If this function is allowed to throw, throw this error. If the client does not handle errors, handle it here."
func maybeThrow() throws? {
guard throw? MyError.oops else {
fatalError("Oops")
}
}
In Swift, the else clause of a guard
statement expects a terminating instruction, like return
or fatalError()
. This control flow is well established, and will enforce that a guard-throw?
always terminates the function -- either via throwing or handling within the else clause.
The floor is open for more syntax considerations.
Calling Optionally Throwing Functions
The compiler should implicitly determine whether the function will throw or provide default handling. This lets callers more flexibly streamline error handling, or opt-in to custom error handling.
Traditionally in Swift, all calls to throwing functions must be rethrown or preceded with try
and handled with a do-try-catch
, if-let-try
, or guard-let-try-else
.
With optionally throwing functions, the above cases will opt to throw errors. If the function is called in a context with unhandled errors, the function will call the default error handling within the else
clause.
func useCustomHandling() {
do {
try maybeThrow() // use throwing function signature since we are "try"-ing
} catch let error {
print("Custom handling: " + error)
}
}
func useDefaultHandling() {
maybeThrow() // use non-throwing function signature since we are not "try"-ing
}
func useRethrow() rethrows {
maybeThrow() // use throwing function signature since we are rethrowing
}
Additional consideration:
func useOptionalRethrow() rethrows? {
maybeThrow() // use optional-throwing function signature; the `useOptionalRethrow()` caller will define context
}
Use case: Array subscripts
Out-of-bounds array subscripts throw an unhandled fatal runtime error. Enforcing error handling for every subscript would break many code bases, but optional throws can give developers a choice to opt-in to error handling.
Currently, the only bound-safe option we have is an if-statement:
func getItem<T>(index: Int, array: Array<T>) -> T? {
if index < array.count {
return array[index]
}
return nil
}
If-statement bound checks are inconsistent with error-handling conventions in Swift. With optional throws, subscript getters can throw an error for handling, instead of forcing a fatal crash.
func getItem<T>(index: Int, array: Array<T>) -> T? {
guard let item = try? array[index] else { return nil }
return item
}
Furthermore, since the function signature is optionally throwing, developers can choose to leave their codebases unaltered and opt to fatally crash, as per the default error-handling implementation of the array subscript getter.
func getItem<T>(index: Int, array: Array<T>) -> T {
return array[index] // crashes with fatal error if out of bounds.
}
Note that with optional throws, all above examples of the array subsript getter are allowed uses.
Discussion
Control Flow Effects
This control flow branch produces different results, depending on the call site. This is potentially confusing when call sites may produce different results from the same call.
func confusingThrow() throws? -> Int {
guard throw? MyError.oops else {
return 42
}
}
func a() -> Int? {
return confusingThrow() // returns 42
}
func b() -> Int? {
return try? confusingThrow() // the same call throws an error and evaluates to return nil
}
One consideration is to force optional-throws to only terminate via fatal crash, retutn nil, or return void when not handled at the call site. This is a different restriction than other guard-else statements, so this is also potentially confusing.
Alternatively, an entirely new syntax may be proposed that more clearly coerces the failed throw? to fatally crash, return nil, or return Void.
However, I personally find this proposal behaves expectedly, and the flexibility of this syntax outweighs the potential confusion.
Single Guarded Expression
Guard statements may contain multiple expressions, however, since throw is an exit instruction, a guard-throw?
may only contain one expression. It is possible to warn other expressions will not be executed, and provide a fix-it to remove all other guarded expressions.