Introduction
Today, Swift's Error type gives great latitude to developers in how they choose to handle errors. That flexibility brings Swift developers a number of benefits but also defers to them a certain level of design and responsibility.
Developers find themselves in a position of deciding exactly what their Error types should and should not include.
Sometimes, that responsibility is itself deferred to a time when it's more convenient. If that time never comes, developers may find themselves in a situation where potentially bug-solving context is lost, erasing all context of a thrown error.
Or, on the other side of the coin, Swift's more dogmatic and 'obsessive/compulsive' constituents (like myself) may find themselves 'encoding their dependence tree' into their Error types in the hope that they don't lose something that they may later find useful for debugging – or perhaps to satisfy their inclination for 'type safety' or 'functional beauty'.
Finally, a developer's chosen solution may not always marry up with the solution and philosophy espoused by another developer across a module boundary.
Solution
My feeling is that the solution to this is actually remarkably simple. Include a special property on Error – 'underlyingError: Error?' – that allows Error to be used as a linked list of increasingly specific context.
Benefits
While the solution is simple, I feel the benefits are wide reaching.
For time pushed application developers this means they can simplify and focus their Error implementations on user-facing messaging and reporting, leaning on the built-in context provided by underlyingError
to log more specific context with no more effort than a variable assignment.
To the more dogmatic and occasionally misguided functional adherents (again, me) we signal, 'perhaps there is no need to constrain your underlying Error types so tightly', whilst still providing all the latitude to perform rigorous control flow.
We also create a cross-boundary contract and expectation – without tight coupling of types – that can provide a developer context into the failure of a third-party module.
Future Direction
Perhaps counterintuitively, I believe that this paves a clearer path to the often raised (pun unintended) typed throws. With the new concurrency changes, the useful scope of the Result type has been reduced and no doubt a proposal for typed throws will appear once again. By including a simple way of defining a chain of thrown errors – and the inevitable utilities that will leverage it – we gently guide developers away from constraining 'all of the things'.
On that note, we also gain a type for which we can build fantastic utilities for logging. Currently, many developers lean on their analytics solutions to track unexpected errors, but I can imagine a counterpart to fatalError(_:)
named something like assertNonFatalError(_:)
that uses this type to produce an adorned stack trace. This could eventually be reported in Xcode alongside fatal crashes, removing a significant impediment for increasing code quality whilst still respecting privacy.
Finally, I'm sure there's some scope for syntactic sugar that will allow module developers who simply wish to re-wrap an underlying error to do so elegantly and concisely. So rather than:
do { try ... }
catch {
let wrappingError = MyError.uhOh
wrappingError.underlyingError = error
throw wrappingError
}
you can:
do { try ... } annotate { throw MyError.uhOh }
And have your new error's underlying type set automatically.
Thanks for reading.