Tool of Thought

APL for the Practical Man

"You don't know how bad your design is until you write about it."

Error Trapping

February 20, 2023

Once we have written some nice, succinct APL code, it is disheartening to muck it all up with error trapping. There are many techniques, and often we just sling ⎕TRAP, :Trap, ⎕SIGNAL and error guards willy-nilly. It would be nice have to some tried and true patterns to apply, and extract as much messiness as possible.

The best error trapping is none at all, or rather, left to functions further up the stack. But if we are providing a package to be used by others, and the API consists of more than utility functions like deleting extraneous blanks, we should provide some minimal distinction between a user error and a bug. This post explores one possible pattern, used in the Text2Date project.

For some packages, there is no need to invent new error numbers and event messages. However, we need a way to distinguish between, say, a length error that we anticipate, and one that we don't. Two functions will suffice to extract all the needed logic:

Signal←{
     ⎕SIGNAL⊂('EN'(⍺+500))('Message'⍵)
 }

and

f←ReSignal
f←⎕SIGNAL{
    ⊂('EN'(⍵.EN-500))('Message'⍵.Message)
}

The Signal function wraps ⎕SIGNAL, and specifies the error number and message for ⎕DMX. The error number is bumped up by an arbitrary multiple of 100 (from 100 to 900), to distinguish it from an unexpected error. So, for example, if the left argument to an API function requires a 1 or 0, to signal a DOMAIN ERROR we might write:

~⍺∊0 1:11 Signal 'The left argument must be 0 or 1'

Note that Signal reverses the arguments of ⎕SIGNAL. This is because ⎕SIGNAL is more often than not called with as the right argument is almost always a literal scalar, while the left argument is often constructed.

At the top of any API function we include the error guard:

500+⍳100::ReSignal ⎕DMX

This traps all errors in our arbitrary block of 100, and using ReSignal propagates expected errors to the calling function. Unexpected errors remain untrapped and fail where they are. ReSignal only specifies the event number EN and Message for ⎕DMX. The event message EM is automatically provided. ReSignal is a niladic trad function that returns a function. The reason for this subterfuge is that we want to encapsulate the right side of this error guard:

300+⍳100::⎕SIGNAL ⊂('EN'(⎕DMX.EN-500))('Message'⎕DMX.Message)

as this is far too much code to be copying and pasting all over the place. But ⎕SIGNAL itself cannot be wrapped inside a function (trad or dfn) or it will not break out of the main function. It must signal in the scope of the main API function. The code to the right of ⎕SIGNAL could easily be wrapped, but it would be nice to include the whole thing. Thus the trad function ReSignal returns a function that includes the whole thing, but keeps the ⎕SIGNAL out of the subfunction. This encapsulation technique is lifted from Adám Brudzewsky.