Lost Computation
By Artyom Bologov
It immensely saddens me when the computation and context is lost. Because it’s there for a reason. More context means better error messages and debugging state preservation. And these result in better developer and user experience. Yet we programmers constantly lose running computation and its context. What a waste.
Case Study: a Web Server #
So imagine a simple Web server. A set of request handlers, thread pool, some dispatcher running handlers on requests. Every handler does work on the request, like querying/modifying DB, crunching numbers, or whatever. Handlers also rely on session data (cookies, API tokens, browser fingerprints) to make decisions. Clear so far, right?
Now what if something goes wrong at the DB level? An exception/error/condition is thrown. Depending on who’s catching it, the consequences differ:
- Request handler
- Has all the context and can return reasonable error message/status.
But every request handler needs its own
catch
clause, and usually more than one. - Handler dispatcher
- Is more generic than request handler, so lacks some context, like handler stack and interim values. Can only return generic error message/status.
- Thread-specific catches
- Even more generic, lack the whole context of Web Server except for some global stats. 404 and 500.
- A whole application
- No idea what’s going on, I guess I just collapse.
You get the trade-offs: catching errors at the leaves of the app is tedious and repetitive. While catching up the branches is too generic and loses context and running computation.
Solution: Break Loops and Dynamic Environment Handlers #
It’s a shame most programming languages adopted the exception-driven stack-unwinding model of errors. Because these are to blame for lost state. Luckily, some programming systems have more reliable error handling that doesn’t lose state/compute.
Common Lisp, for example.
There’s this handler-bind
block that catches an error and acts in its dynamic environment.
Meaning: all the state of e.g. erroring request is there for the handler to act on.
With the ability to restart
the whole computation from where it stopped once you fix the state.
Sometimes even interactively, if the user wants to.
I know this type of error environment inspection is a luxury that most languages don’t provide. But it’s also a proof for what’s possible, so why settle for less?
Another Solution: Context Passing #
But what if we didn’t throw at all? If we return error values, then we don’t lose context we can store in them. That’s what Go (second return value,) Haskell (Left/Right,) and Clojure (context maps) settled on.
Dave Liepmann wrote on Clojure’s approaches to errors. So I’ll just refer you to his post for a better overview.
The problem is that one has to explicitly pass the state along with an error. And no one wants to do that. So this approach is worse than the automatic environment inspection that Lisp allows. But still better than destroying all the state.
Whatever way to preserve the erroring state you pick, I beg of you. Please, don’t lose computation.