First AsyncSteps

By convention, JS version of AsyncSteps module is referred as $as.

Note: there is no “thread” term in the spec - it used only for the guide..

Basic “thread”

Let’s start from AsyncSteps “thread” creation:

const $as = require( 'futoin-asyncsteps' );
const root_as = $as(); // create "thread"

Note: $as() call is syntax sugar for new AsyncSteps

Then, let’s add some steps through .add() API:

root_as.add( (asi) => console.log( 'Hello World!' ) );

Finally, let’s start the “thread”:

// only allowed on root instance
root_as.execute(); // start the "thread"

The calls above can be chained:

$as()
    .add( (asi) => console.log( 'Hello World!' ) )
    .execute();

Steps

“Step” is continuously executed code fragment. Each such step can add other “inner” steps during execution.

Step is represented by a callable. It must always take asi interface as the first argument. asi is a single object to expose all AsyncSteps features during execution of particular step.

asi.add( (asi) => {
    // code fragment
} );

Result passing

The step may take additional parameters. They are populated with explicit result (as.success()) of the previous executed step.

asi.add( (asi) => {
    asi.add( (asi) => asi.success( 1, 'a', {} ) );
    asi.add( (asi, n, s, o) => {} );
} );

Thread local storage

A single instance of implementation-defined state is created with root AsyncSteps instance. It must be always accessible through asi.state.

asi.add( (asi) => {
    asi.add( (asi) => {
        asi.state.some_field = {};
        asi.state.other_field = 123;
    } );
    asi.add( (asi) => {
        console.log( asi.state.some_field );
    } );
} );

Note: please avoid passing generic results through state object!

Throwing errors

Expected errors are triggered by asi.error( error_code[, error_info] ). It sets internal state and throws exception to ensure interruption of current step execution. So, unexpected exceptions also get processed.

Error code must be a string which does not change over software evolution. String type has been choosen to be easily passable through network. It can be seen as name of exception class.

Error info must be a string as well. It is saved in state.error_info.

Exception thrown is saved in state.last_exception.

asi.add( (asi) => {
    asi.error( 'ErrorCode', 'Some arbitrary info' );
} );

Error handling

Each step can be accompanied by optional error handler. The step itself can be seen as try {} block and the error handler as catch {} block.

Error handler is set through optional second parameter of asi.add().

Error handler can:

  • call asi.success() to ignore the error and continue execution,
  • call asi.error() to override the error,
  • call asi.add() to make recovery actions and continue execution,
  • otherwise, the async stack is unwinded with the original error.
asi.add(
    (asi) => call_something( asi ),
    (asi, error_code) => {
        switch ( error_code ) {
        case 'Duplicate':
            as.success(); // ignore
            break;
        case 'Mismatch':
            as.add( (as) => call_other( asi ) ); // recovery
            break;
        case 'SecretError':
            as.error( 'GenericError' );
            break;
        default:
            console.log( as.state.last_exception ); // just observe
            // continue async stack unwinding
        }
    }
);

Semantics error handling & production mode

It’s important to use the AsyncSteps interfaces passed to execution or error handler.

Unless NODE_ENV=production, AsyncSteps validates handler parameter count and performs other sanity checks with descriptive error.

If such errors happen in production then most likely there still be a failure, but error message would be quite cryptic to understand.