async-transport
v1.0.7
Published
[![NPM version][npm-image]][npm-url] [![Travis Build][travis-image]][travis-url]
Downloads
13
Maintainers
Readme
Async Transport
Provides support for 1-n asynchronous, returning a predictable object for easy consumption.
Contents
Pre-requisites
You need to use Node 7.6 (or later) or an ES7 transpiler to use async/await functionality. You can use babel or typescript for that.
Install
npm i async-transport --save
or using yarn
yarn add async-transport
Why use asyncTransport?
ES7 async/await allows us to write asynchronous code in a style that looks synchronous. This is great, but under the hood, it is crucial to remember that we are still effectively dealing with Promises. When implementing error handling inside Promises developers commonly use a try/catch approach like the following.
async function asyncTask(cb) {
try {
const user = await UserModel.findById(1);
if(!user) return cb('No user found');
} catch(e) {
return cb('Unexpected error occurred');
}
try {
const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
} catch(e) {
return cb('Error occurred while saving task');
}
if(user.notificationsEnabled) {
try {
await NotificationService.sendNotification(user.id, 'Task Created');
} catch(e) {
return cb('Error while sending notification');
}
}
if(savedTask.assignedUser.id !== user.id) {
try {
await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
} catch(e) {
return cb('Error while sending notification');
}
}
cb(null, savedTask);
}
This becomes onerous, for example, when writing API endpoints that assemble data from multiple sources and may quickly become hard to read (and maintain). When researching ways to remedy this, I found this Medium blog entry written by Dima Grossman and began using the resulting await-to-js package. This yielded code like so.
import to from 'await-to-js';
async function asyncTask() {
let err, user, savedTask;
[err, user] = await to(UserModel.findById(1));
if(!user) throw new CustomerError('No user found');
[err, savedTask] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
if(err) throw new CustomError('Error occurred while saving task');
if(user.notificationsEnabled) {
const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
if (err) console.error('Just log the error and continue flow');
}
}
This was an improvement, but still did not fully meet my needs. In more complex use-cases, one still needed to manage and track multiple arrays as a result of wrapping async code in to
method calls.
What if we could utilize a single method to handle the execution of 1-n async functions, using to
behind the scenes to keep things cleanly organized asyncTransport
was my answer to this question.
Utilizing asyncTransport
, the above code may be further simplified like so.
import asyncTransport from 'async-transport';
async function asyncTask() {
let result = await asyncTransport([
UserModel.findById(1),
TaskModel({userId: user.id, name: 'Demo Task'}),
NotificationService.sendNotification(user.id, 'Task Created')
]);
// Print (or throw) errors by iterating over the errors collection
if (result.hasErrors) {
for (let i = 0; i < result.errors.length; i++) {
if (result.errors[i]) { console.log(result.errors[i]); }
}
// Otherwise do stuff with the result data
} else {
console.log(result.data);
}
}
Using this approach, we only have to track one variable, result
, which will reliably contain three properties, hasErrors
, errors
, and data
.
errors
and data
are parallel arrays, both of which maintain the order of the functions provided. In other words, executing asyncTransport([fn1, fn2, fn3])
will return an object whose errors
contain [errors-from-fn1, errors-from-fn2, errors-from-fn3]
as well as a data
array that contains [data-from-fn1, data-from-fn2, data-from-fn3]
.
Details
The asyncTransport
function takes two arguments
promiseCollection
- an array of functions, if a single fn is passed, it will be converted to an array with a single element.options
- an object containing params that affectasyncTransport
's execution. Currently only thestrategy
param is supported (see more on this below in the usage examples).
Once all functions in the promiseCollection
have either resolved or errored (or some combination of the two), an object is returned containing 3 props. These props are
hasErrors
-true
if errors were detected,false
otherwiseerrors
- an array of errors organized in the order in which the related functions were passeddata
- an array of data objects organized in the order in which the related functions to were passed
Usage Examples
While these examples are clearly trivial and fabricated, they should clearly demonstrate how asyncTransport
may be integrated into your work-flow.
Basic Examples
If you are concerned with only one async fn, you could write something like the following.
const asyncTransport = require('async-transport');
// One mock async fn
let asyncThing1 = () => {
let deferred = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ foo: 'bar' });
}, 100)
});
return deferred;
}
// Working with promises via async/await
let foo = async () => {
let theData = await asyncTransport(asyncThing1);
console.log(theData);
}
foo();
// The above code logs the following object to the console
{
hasErrors: false,
errors: [ null ],
data: [ { foo: 'bar' } ]
}
If you have multiple async fns simply pass an array to asyncTransport
like so.
const asyncTransport = require('async-transport');
// Two mock async fns
let asyncThing1 = () => {
let deferred = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ foo: 'bar' });
}, 100)
});
return deferred;
}
let asyncThing2 = () => {
let deferred = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ baz: 'boom' });
}, 50)
});
return deferred;
}
// Working with promises via async/await
let foo = async () => {
let theData = await asyncTransport([ asyncThing1, asyncThing2 ]);
console.log(theData);
}
foo();
// Above code logs the following object to the console
{
hasErrors: false,
errors: [ null, null ],
data: [ { foo: 'bar' }, { baz: 'boom' } ]
}
Serial Execution Examples
By default, asyncTransport
will execute async fns passed to it (nearly) simultaneously and items will resolve in whatever order they resolve.
If each function relies on the previous fn to have successfully resolved, you can pass a second parameter to asyncTransport
to enforce serial execution of async functions. Let's modify our example above to see how we do this.
const asyncTransport = require('async-transport');
let asyncThing1 = () => {
let deferred = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ foo: 'bar' });
}, 100)
});
return deferred;
}
let asyncThing2 = () => {
let deferred = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ baz: 'boom' });
}, 50)
});
return deferred;
}
let foo = async () => {
let theData = await asyncTransport([
asyncThing1,
asyncThing2
],
{strategy: 'serial'} // Ensures fns resolve in the order they are passed
);
console.log(theData);
}
foo();
// Above code still logs the following object to the console, but
// output in this case is identical to the `parallel` strategy
{
hasErrors: false,
errors: [ null, null ],
data: [ { foo: 'bar' }, { baz: 'boom' } ]
}
Data Dependent Serial Execution Examples
In addition to guaranteed order of execution, asyncTransport
also provides a way to pass results from prior async function calls to subsequent functions as arguments. Consider the following.
const asyncTransport = require('async-transport');
let asyncThing3 = () => {
let deferred = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
val: 4
});
}, 75)
});
return deferred;
}
// args parameter is the data object returned from the prior async fn
let asyncThing4 = (args) => {
let deferred = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
val: args.val * 2 // multiply 4 * 2 (see execution below)
});
}, 20)
});
return deferred;
}
let foo = async () => {
let theData = await asyncTransport([
asyncThing3,
asyncThing4
],
{strategy: 'serial'}
);
console.log(theData);
}
foo();
// Above code logs the following object to the console
{
hasErrors: false,
errors: [ null, null ],
data: [ { val: 4 }, { val: 8 } ]
}
Given the above, ideally one should add safeguards - like checking that args
and any keys you plan on using in your fn actually exist - but as written, it should be adequate to illustrate the process.
License
MIT © Joseph (Jos) Smith