try-catch-finally style error management is common in many programming
languages. Though the underlying mechanism of propagating errors up a “call
stack” is alright from a development perspective, the common syntax ends up
invariably mangling the code flow. In cspjs, a macro library presenting an
felt to me to be a better way to fit error handling and recovery code into the
statement-by-statement sequential flow of activity.
Given code like above,
statement k is executed only after
(We’ll ignore conditional branches and looping for now.)
This lets us draw the following picture, where the
NOW line steps through the
statements one at a time.
The rough mental model of such “statement execution” used by programmers also has the same temporal order. We think about what “statement 1” should do, then about what “statement 2” should do, and so on as we actually write code. We plan ahead before writing such code (hopefully!), but right in the middle of the act itself, at least for me, the time order of the activities dominates.
The dangling catch
I’d argued in my previous post that it doesn’t make sense to have most code outside of a try block. If we assume that, then we can simplify the error handler writing as follows -
This composes well with multiple such guard handlers since the code flow reflects the temporal order of the impact of the code.
The job of a
catch guard block is to improve the reliability of the actions
that temporally (and textually) follow it, from the perspective of those
actions that precede it.
In cspjs, the scope of such a
catch is the inner-most block scope -
When dealing with unreliable code, one way to improve reliability is to retry actions until they succeed, or until you’ve “tried enough”. For example, if these actions are failing due to an unreliable network connection, an exponential backoff may be used to improve its reliability and recoverability.
Say the “handler” in the first example wishes to try the following actions again. This is expressed in cspjs as -
retry; statement would result in an infinite loop,
but other than that there isn’t any restriction on where within a
retry; statement can be placed. The retry will return control
statement 3; and onwards if ther retry condition is met. The purpose
of such a catch guard is to not let some significant work done by statements
1 and 2 go to waste as far as possible.
In order for a
catch block to be able to improve the reliability of the
activity that follows it, especially if it wants a
retry, then the handler
block needs to assume that the state of the system is pretty much how it was
statement 3; is about to be entered. If it cannot make that assumption,
then the code for such a handler would have to deal with way too many cases
before it can make a retry.
In order for this assumption to be valid, any resources grabbed by the previous
invocation of the sequence must be released before the handler takes effect.
This is done in cspjs using “dangling finally” clauses, which are also
block scoped, like the
catch clauses. These dangling finally clauses are
defer in golang.
The “release resource ..” statements will get to run once the scope in which
it is embedded closes - i.e. after
statement n; completes either successfully,
or some error occurs between statements 5-n.
When an error occurs in
statement 6;, say, it is easy to trace which finally
clauses execute before the
catch handler is hit - just trace back the steps
catch clauses can also be used to release resources, they will be
called only under error circumstances. Therefore when it comes to resource
finally is almost always the right choice.
Finally some subtleties
If you treat such a
finally clause itself as a statement, then it cannot
make any assumptions about how the statements following it (in time and in
code) modify the variables constituting the system state as these steps proceed.
Here is an example (using cspjs syntax) -
On cursory reading of the above code, one might suspect that the finally
clauses will close “file2.txt” twice, since the state variable
f is being
reused to open the second file. That’s clearly undesirable.
Therefore when the first
finally block executes, it must make sure that the
f it refers to is the first file - i.e. the value of the
f at the time the
finally block is encountered, rather than the value “now”, whenever “now”
happens to be. To ensure this, the state machine to which cspjs compiles
async tasks saves the state variables and restores them before processing the
cleanup steps. The above code expands to something like this in principle -
The end result is that you don’t have to worry about these details and it “does the right thing”.
Finally, finally statements
The code in the previous section can also be written like this -
i.e. a statement form of
finally is also supported, with slightly different
behaviour than the block form. The statement form evaluates all variables (including
arguments to the invocation), but postpones the actual call until later. If a
resource release is a single statement, this statement form can be used.
I hope the above helped clarify the motivation behind changing the syntax of
finally so that they respect the temporal nature of code flow
that we’re used to in sequential programming.
The statement form of
finallylooks and behaves somewhat like
deferin Go except for being block scoped, but the origin is older than that. Though I’ve forgotten the original inspiration, it features in a scheme dialect I built a few years before Go was announced.
I borrowed the word
awaitin cspjs from C#’s