SkillAgentSearch skills...

Asynquence

Asynchronous flow control (promises, generators, observables, CSP, etc)

Install / Use

/learn @getify/Asynquence

README

asynquence

CDNJS

Promise-style async sequence flow control.

Explanation

asynquence ("async" + "sequence") is an abstraction on top of promises (promise chains) that lets you express flow control steps with callbacks, promises, or generators.


If you're interested in detailed discussion about asynquence, here's some reading to check out:


TL;DR: By Example

Sequences

Say you want to perform two or more asynchronous tasks one after the other (like animation delays, XHR calls, file I/O, etc). You need to set up an ordered series of tasks and make sure the previous one finishes before the next one is processed. You need a sequence.

You create a sequence by calling ASQ(...). Each time you call ASQ(), you create a new, separate sequence.

To create a new step, simply call then(...) with a function. That function will be executed when that step is ready to be processed, and it will be passed as its first parameter the completion trigger. Subsequent parameters, if any, will be any messages passed on from the immediately previous step.

The completion trigger that your step function(s) receive can be called directly to indicate success, or you can add the fail flag (see examples below) to indicate failure of that step. In either case, you can pass one or more messages onto the next step (or the next failure handler) by simply adding them as parameters to the call.

Example:

ASQ(21)
.then(function(done,msg){
    setTimeout(function(){
        done(msg * 2);
    },10);
})
.then(function(done,msg){
    done("Meaning of life: " + msg);
})
.then(function(done,msg){
   msg; // "Meaning of life: 42"
});

Note: then(..) can also receive other asynquence sequence instances directly, just as seq(..) can (see below). When you call then(Sq), the Sq sequence is tapped immediately, but the success/error message streams of Sq will be unaffected, meaning Sq can be continued separately.

If you register a step using then(...) on a sequence which is already currently complete, that step will be processed at the next opportunity. Otherwise, calls to then(...) will be queued up until that step is ready for processing.

You can register multiple steps, and multiple failure handlers. However, messages from a previous step (success or failure completion) will only be passed to the immediately next registered step (or the next failure handler). If you want to propagate along a message through multiple steps, you must do so yourself by making sure you re-pass the received message at each step completion.

To listen for any step failing, call or(...) (or alias onerror(..)) on your sequence to register a failure callback. You can call or() / onerror(..) as many times as you would like. If you call it on a sequence that has already been flagged as failed, the callback you specify will just be executed at the next opportunity.

ASQ(function(done){
    done.fail("Failed!");
})
// could use `or(..)` or `onerror(..)` here
.or(function(err){
    console.log(err); // Failed!
});

Gates

If you have two or more tasks to perform at the same time, but want to wait for them all to complete before moving on, you need a gate.

Calling gate(..) (or alias all(..) if you're from the Promises camp) with two or more functions creates a step that is a parallel gate across those functions, such that the single step in question isn't complete until all segments of the parallel gate are successfully complete.

For parallel gate steps, each segment of that gate will receive a copy of the message(s) passed from the previous step. Also, all messages from the segments of this gate will be passed along to the next step (or the next failure handler, in the case of a gate segment indicating a failure).

Example:

ASQ("message")
.all( // or `.gate(..)`
    function(done,msg){
        setTimeout(function(){
            done(msg + " 1");
        },200);
    },
    function(done,msg){
        setTimeout(function(){
            done(msg + " 2");
        },10);
    }
)
.val(function(msg1,msg2){
    msg1; // "message 1"
    msg2; // "message 2"
});

all(..) (or gate(..)) can also receive (instead of a function to act as a segment) just a regular asynquence sequence instance as a gate segment. When you call all(Sq), the Sq sequence is tapped immediately, but the success/error message streams of Sq will be unaffected, meaning Sq can be continued separately.

Handling Failures & Errors

Whenever a sequence goes into the error state, any error handlers on that sequence (or any sequence that it's been pipe()d to -- see Conveniences below) registered with or(..) will be fired. Even registering or(..) handlers after a sequence is already in the error state will also queue them to be fired (async, on the next event loop turn).

Errors can be programmatic failures (see above) or they can be uncaught JS errors such as ReferenceError or TypeError:

ASQ(function(done){
    foo();
})
.or(function(err){
    console.log(err); // ReferenceError: foo is not defined
});

In general, you should always register an error handler on a sequence, so as to catch any failures or errors gracefully. If there's no handlers registered when an error or failure is encountered, the default behavior of the sequence is to throw a global error (unfortunately not catchable with try..catch).

ASQ(function(done){
    foo();
});

// (global) Uncaught ReferenceError: foo is not defined

However, there will be plenty of cases where you construct a sequence and fully intend to register a handler at a later time, or wire it into another sequence (using pipe() or seq()-- see Conveniences below), and these sequences might be intended to latently hold an error without noisily reporting it until that later time.

In those cases, where you know what you're doing, you can opt-out of the globally thrown error condition just described by calling defer() on the sequence:

var failedSeq = ASQ(function(done){
    done.fail("Failed!");
})
// opt-out of global error reporting for
// this sequence!
.defer();

// later
ASQ(..)
.seq(failedSeq)
.or(function(err){
   console.log(err); // Failed!
});

Don't defer() a sequence's global error reporting unless you know what you're doing and that you'll definitely have its error stream wired into another sequence at some point. Otherwise, you'll miss errors that will be silently swallowed, and that makes everyone sad.

Conveniences

There are a few convenience methods on the API, as well:

  • pipe(..) takes one or more completion triggers from other sequences, treating each one as a separate step in the sequence in question. These completion triggers will, in turn, be piped both the success and failure streams from the sequence.

    Sq.pipe(done) is sugar short-hand for Sq.then(done).or(done.fail).

  • seq(..) takes one or more functions, treating each one as a separate step in the sequence in question. These functions are expected to return new sequences, from which, in turn, both the success and failure streams will be piped back to the sequence in question.

    seq(Fn) is sugar short-hand for then(function(done){ Fn.apply(null,[].slice.call(arguments,1)).pipe(done); }).

    This method will also accept asynquence sequence instances directly. seq(Sq) is (sort-of) sugar short-hand for then(function(done){ Sq.pipe(done); }). Note: the Sq sequence is tapped immed

Related Skills

View on GitHub
GitHub Stars1.7k
CategoryDevelopment
Updated1mo ago
Forks148

Languages

JavaScript

Security Score

85/100

Audited on Feb 19, 2026

No findings