The Queue

Getting Started

As seen in the intro examples, trying to coordinate multiple async functions into a workflow can be convoluted and hard to follow. Parallel/Concurrent function execution requires extra steps to await promises returned from the previously executed async functions. In addition to this, error handling via try/catch blocks can often become a daunting process. In some instances it's desirable to catch errors, in others, throwing fatal errors is a must. And yet other cases only certain types of errors should be thrown. Also, async functions that need to run in the background require wrapping them within another async function in order to handle errors since await is not used and an error may occur during actual execution. Furthermore, additional corriagraphy logic is required for contextual workflow routing (i.e. branching) that is typically offered by workflow engines.

Asynchro was developed to address these issues and other nuances encountered when working with multiple async functions. Simplicity is the core concept that keeps asynchro concise and intuitive without being bombarded with bloated features that will rarely, if ever, be used.

The Queue

At the heart of asynchro is a queue. The queue is simply an Object[] that holds function execution metadata until Asynchro.run is executed/ran. A single result object can be passed to the Asynchro constructor in order to store the results from each queued async function execution using a designated name as the property name in the result object (more on this later!). Each queued async function is ran in the order that it was queued. Although there are variations of each, there are only three types of async functions that can be queued:

  1. Series - async functions that are awaited for until the next function in the queue is invoked (can also be normal synchronous functions)
  2. Parallel - async functions that are not immediately awaited for, but rather awaited for after the queue is exhausted
  3. Background - like, parallel, but will not be awaited for and will still retain error handling set on the queued function (or error handling defined globally on the Asynchro instance itself)

Let's review the simple workflow below:

  const ax = new Asynchro({}, false, console.log);
  ax.series('one', mySeriesFunc1, 'val1', 2, 3);
  ax.series('two', mySeriesFunc2, 1, 2);
  ax.parallel('three', myParallelFunc1, { a: 1, b: 2, c: 3 });
  ax.parallel('four', myParallelFunc2);
  ax.parallelThrowOverride('five', true, myParallelFunc3, 'always throw errors');
  ax.series('six', mySeriesFunc3, true, false);
  const result = await ax.run();
  return { result, errors: ax.errors };

The Asynchro constructor is called by passing a result object as it's 1st argument where all of the results of each queued async function will be stored. For instance, the final results returned from Asynchro.run (or accessed by Asynchro.result) would consist of the following assuming no errors are thrown:

// Asynchro.result
{
  one: 'return string from mySeriesFunc1',
  two: { myString: 'mySeriesFunc2 returned an object' }
  three: 'results can be anything',
  four: '...another result',
  five: true,
  six: 123
}

Each argument passed after the async function will be passed in order to that function. For example, mySeriesFunc1/"one" would receive val1, 2, 3. To omit an async function's return value from the final result simply use null, false or undefined as the name argument value: ax.series(null, mySeriesFunc1, 'val1', 2, 3);.

Passing Results During Execution

Passing results from one async function to the next is fairly easy to follow using Asynchro.arg:

const asyncOne = async () => {
  // other async operations here
  return { array: [1] };
};
const asyncTwo = async (array) => {
  // other async operations here
  const rtn = 2;
  array.push(rtn);
  return rtn;
};
const asyncThree = async (a1) => {
  // other async operations here
  console.log(a1); // prints out 1
  return a1 + 2;
};
const asyncFour = async (array) => {
  // other async operations here
  array.push(4);
};

const ax = new Asynchro({});
ax.series('one', asyncOne);
ax.series('two', asyncTwo, ax.arg('one.array'));
ax.series('three', asyncThree, ax.arg('one.array[0]'));
ax.series(null, asyncFour, ax.arg('one.array'));
const rslt = await ax.run();
console.log(rslt); // { one: { array: [1, 2, 4] }, two: 2, three: 3 }

Keep in mind that result arguments may not be available in a parallel async function when referencing a previously queued parallel async function execution (depending on how long the previous operation takes):

const asyncOne = async () => {
  // other async operations here
  return { array: [1] };
};
const asyncTwo = async (array) => {
  // other async operations here
  array.push(2); // ERROR: one.array not yet set!
};

const ax = new Asynchro({});
ax.parallel('one', asyncOne);
ax.parallel(null, asyncTwo, ax.arg('one.array'));
await ax.run();

Error Handling

You may have noticed that the 2nd argument passed into the Asynchro constructor was explicitly set to false (which is the default value). So, no errors will be thrown but rather captured in an Error[] via Asynchro.errors unless explicitly overridden by using Asynchro.seriesThrowOverride or Asynchro.parallelThrowOverride as seen in example for myParallelFunc3/"five" where true was used. This is referred to as an Error Handling Rule. Now let's assume that there was an error while awaiting mySeriesFunc2/"two". The queue would catch/capture the error and continue to execute subsequent async functions in the queue. Each error will contain additional metadata under an Asynchro property that will provide more details about the error:

// Asynchro.result
{
  one: 'return string from mySeriesFunc1',
  three: 'results can be anything',
  four: '...another result',
  five: true,
  six: 123
}
// Asynchro.errors
[
  { // the error object
    // ... other error properties here
    Asynchro: {
      name: 'two',
      operation: 'mySeriesFunc2',
      event: false,
      isPending: false,
      isParallel: false,
      isBackground: false
    }
  }
]

Assuming that the Error Handling Rule was set to true, the same error would have been thrown and would have contained the same Asynchro metadata. But what if we only want to throw specific errors- like "system" errors? Asynchro provides a way to define what errors are thrown and what errors are caught/captured by using an Object descriptor as the Error Handling Rule instead of the Boolean values previously discussed. To throw only "system" errors defined by Asynchro.systemErrorTypes, the Error Handling Rule can be set to the following Object descriptor:

{
  invert: false, // true to catch errors when matches are made, false/omit to throw errors when matches are made
  matches: 'system' // only errors that are an instanceof the predefined "system" error classes will be thrown 
}

To throw all errors, but "system" errors defined by Asynchro.systemErrorTypes, set invert = true. Also, matches can be set to an Array of types/classes that will be used when checking if the Error is an instanceof any one of the defined entries:

{
  matches: [ RangeError, MyCustomError ] // only errors that are an instanceof RangeError or MyCustomError will be thrown 
}

Another way to control which errors are caught or thrown is to use an Object as the matches value. Any error that contains all of the properties/values defined on that Object will be thrown (or alternatively caught when invert = true):

{
  matches: { // only errors that contain someProperty1 = true and someProperty2 = false will be thrown
    someProperty1: true,
    someProperty2: false
  } 
}

For convenience, Asynchro.throwsError is provided to check if the queue will throw or catch a specified Error/error type.

Background Tasks >>


4.0.0 (2019-12-20)

Full Changelog

Features: