I agree with your point for the general case. Most of the time if something underneath you can fail, it can fail in new and exciting ways that are impossible to fully enumerate ahead of time. Things can go wrong in more ways than they can go right. Strong and fully-enumerated types are great for reasoning about the well-behavior of systems, but can make reasoning about the mis-behavior of systems harder. We really want to avoid error mappings, continuously churning taxonomies, and especially things like a case unknown(Error)
which suffers the worst of both worlds.
That being said, typed errors would be really really nice for libraries whose operations can fail in well-defined and limited ways. For System, which is both a binary stable and source stable library, we held the party line that throws
is the Swifty expression of failure and we'd have to find some workaround for the error boxing issues. Here's the workaround we found:
extension FileDescriptor {
@_alwaysEmitIntoClient
public func close() throws { try _close().get() }
@usableFromInline
internal func _close() -> Result<(), Errno> { ... }
}
Our ABI is expressed in terms of Result
for performance but our API is expressed in terms of throws
. That is, emit the Result -> throws mapping into user code hoping that the optimizer can learn to unbox the Int32
(Errno
here is just a wrapper around In32
). We have @_alwaysEmitIntoClient
instead of @inlinable
because we want to eject the name close
from our ABI in case Swift ever does get typed throws: removing or warning on an unneeded catch statement is more feasible than breaking binary compatibility.
Outside of throwing Errnos, there are still several uses for typed throws. They're useful for higher-fidelity alternatives to things like failable initializers and preconditions.
For example, inside the standard library we have String initializers that decode UTF-8 and either fix up encoding errors as they go, or else return nil
if there are errors. We don't have a good way to communicate why a decode went wrong from an initializer. We could add a static method that returns a Result<String, UTF8DecodingError>
, but typed throws
on an initializer would be Swiftier. We don't want untyped throws
on the init, because the fact that failure produces a DecodingError
is part of the API's contract and we would consider throwing anything else to be a hard break of both source and binary compatibility. Untyped throws gives the users a false impression that something else might be thrown in the future and it gives the library maintainer the false impression that they could throw something else in the future. It also makes the catch sites ugly.
Over in the part of System that does not wrap POSIX calls, we have another (related) use case for typed throws. FilePath.Component
can be created from a String literal but contains the precondition that the string is not empty nor does it contain a directory separator (/
), i.e. it is exactly one component. It also has a failable initializer that will just returns nil
. It would be nice to have something that did both, a way to fail that carries the precondition message up the stack with it. Here we want to throw a concrete error, and we expect it to commonly be automatically propagated upwards as an untyped error.
In System, throws
currently carries a connotation that the underlying system is consulted (since syscalls can fail). If we had types on throws
, it would be explicit in the code and checked by the compiler. This would then open us up to using error handling for other errors generated within library code itself.