IO.js
Library for async Javascript with support for error handling and recovery.
Install / Use
/learn @srikumarks/IO.jsREADME
IO.js: A library for composeable asynchronous actions
IO is a Javascript library for managing sequencing of asynchronous actions.
This infoq article summarizes the state of the art in an interview conducted
with the creators of the existing libraries for managing asynchronous
operations in JS and features interviews with the creators of these libraries -
Do, Step, Flow-js, node-promise, Async, Async.js, FuturesJS and
slide-flow-control. What stands out is that most of them say relatively
little on "error management". Here is also a Hacker News thread discussing
Python creator Guido van Rossum's objections to a callback based API, based on
its poor ability to work with exceptions.
The focus of IO, therefore, is to provide for flexible error management, in
particular --
- Trapping errors and deciding what to do with them,
- Recovering from errors and resuming operations,
- Managing control flow between error handlers,
- Showing clearly the scope of control of error handlers that are in effect.
... and, of course, do the rest of the stuff that the other libraries do fairly well.
With regard to efficiency, IO takes the view that the tasks that you're
executing ought to be much more complex than any overheads IO might add.
Core concepts
IO, at its core, is a library for creating "actions" and "running" them, usually
using IO.run(input, action).
Actions
An action is a function that does something, possibly asynchronously, and
chooses what to do next based on what it did. Actions are usually created and
composed using the various functions provided by IO, but you can write your
own as well. You run an action like this -
IO.run("some input data", action);
User supplied actions can come in one of four forms. The forms are detected using the number of arguments that the function has -
-
Ordinary functions of the form --
function (input) { return something; }These actions succeed by returning a result which is passed along, or fail by throwing an exception. If the return value is an action, that action is inserted into the sequence right there, supplying the same given input. This gets us "dynamic actions". If the return value is not an action and is a datum, it is considered to be the output of this action that is to be passed to the steps further ahead in the sequence. If the return value is
undefined, the execution sequence stops right there. -
Pure action of the form --
function (callback, errback) { ... }This is in the common "callback/errback" style where the callback is a one argument function used for continuing with the output of this action and errback is a one argument function that starts an error processing sequence.
-
Input processing form --
function (input, callback, errback) { ... }The input flows in at the point the action is executed and
callbackanderrbackare as described above. You'll mostly use the previous form and this form. -
Fully customizeable form --
function (M, input, success, failure) { ... }Mis the currently active orchestrator,inputis the input available at the point the action is executed. The most important point to note is this --successandfailureare also actions in this form.You'll rarely need this, but this allows you to change orchestrators on the fly, start other action sequences using the "current orchestrator", whatever that happens to be, tweak the control flow by affecting what comes before or after
successandfailure, etc. This form permits actions to be composed inIOand helps separate "what to do" from "when it is being done". If you want to trap the full continuations at any point to do something strange with them, you can use this form.
Orchestrators
IO provides (currently) two ways to run actions -- IO.run and IO.trace.
These two correspond to the IO.Ex and IO.Tracer objects called "orchestrators".
Orchestrators are used for customizing the execution pipeline. (Note: This is
still work-in-progress and only some basic functionality is available for
customization.)
-
IO.run(input, action)will run the action normally, passing the given input object to it. It is an alias forIO.Ex.IO.run(input, action) = IO.Ex.run(input, action) -
If you want to trace the steps involved as an action runs on
console.log, you can turn an action into a traced action usingIO.tracelike this -IO.trace(action). So if you runIO.run(input, IO.trace(action)), you'll get tracing output on the console.IO.run(input, IO.trace(action)) = IO.Tracer(IO.Ex).run(input, action)IO.traceworks by changing the orchestrator on the fly to a tracer built on the original orchestrator used to run the action. The semantics of the action aren't affected by the insertion of the trace. This design, as opposed to merely exposing the tracer, also lets you trace selected portions of a longer action sequence. You can useIO.traceas a replacement forIO.do.
Core actions
IO.do(actions)
Makes an action that performs the given actions in the given order, passing the output of each action to the next. The resulting compound action can be further composed with other actions.
IO.do(a1, a2, ...)
IO.do([a1, a2, ...])
IO.try(actions, handler)
An action that performs the given actions and if any failure occurs, deems the actions to have failed and passes the error to the handler. The handler is joined to whatever follows after the try and can therefore continue by simply succeeding. If the handler fails, the whole try is considered to fail.
IO.try(action, handler)
IO.try([a1, a2, ...], handler)
IO.try(IO.log("some action"), IO.log("oops! Here is the error - ", true))
IO.alt(actions)
Short for "alternatives". The actions are tried in sequence and the first one
to succeed passes its output to what follows the IO.alt. The whole alt
action is semantically the same as that succeeding action. All actions receive
the same input, unlike IO.try where the handler receives the error object
of the failed action.
IO.alt(a1, a2, ...)
IO.alt([a1, a2, ...])
IO.raise(info)
Raises an in-sequence error meant for handling by whatever handlers have been
setup. The info is arbitrary and is just passed along with the error object
in the error field.
IO.raise("some error object")
IO.catch(onerror)
Sets up a "catch point" for trapping errors raised using IO.raise. This is
useful for implementing commit-rollback semantics. onerror is itself an action.
The closest catch point gets to have a go first and can do a variety of things -
-
Decide that it cannot handle the error and pass on to catch points "higher up". To do this, the handler must "fail".
IO.catch(IO.fail) IO.catch(function (err, restart, giveup) { // ... giveup("some reason, maybe?"); }) -
Do something and try the sequence of actions immediately following this catch point once more. This is called a "restart". You can even setup loops this way. To do this, the handler must "succeed".
IO.catch(function (err, restart, giveup) { // do something restart("new input"); }) // Ex: This does an infinite restart loop. IO.do(IO.log("one") , IO.catch(function (err, restart, giveup) { restart("again"); }) , IO.log("two") , IO.raise("forever")) -
Take some corrective action and resume from the
raisepoint as though it succeeded. This "resume" action is available as the "resume" field of the error object, which you can call likeerror.resume(value)and the given value will be injected there.IO.catch(function (err, restart, giveup) { // take corrective action here err.resume("new input"); }) // Ex: IO.run("input", IO.trace(IO.log("one") , IO.log("two") , IO.catch(function (err, restart, giveup) { err.resume("YAY!"); }) , IO.log("three") , IO.raise("BOMB!") , IO.log("surprise!"))) -
Do the "rollback" sequence again from the error point. This action is available in the error object and is invoked as
error.rollback(value). The value you pass to the rollback function will usually be the error object itself.IO.catch(function (err, restart, giveup) { // Oh, we figured we can retry! err.rollback(err); }) -
Deep customization relative to the error point is available through the
successandfailureactions stored in the error object. You can use this to, for example, change what happens before or after theresumecompletes, for example, log an error in a database.IO.catch(function (M, err, success, failure) { // Note that 'success' and 'failure' are complete // actions in themselves and not in the // "callback/errback" one-argument style. M.call(some_compl
