Making Promises: Asynchronous code in JavaScript
March 19, 2018
Introduction
In more complex applications, especially sever side applications that leverage asynchronous operations, the usage of callbacks does not allow for efficient event handling, and can cause issues with how things are handled within an app (timing problems, overloading, blocking operations).
The introduction of Promises and Generators (commonly known as co-routines) in JavaScript flipped everything on it’s head. Promises in JavaScript allow better control of how asynchronous code is handled in an application, and allows you to leverage several amazing features of the V8 engine.
What is Asynchronous code?
Without over-complicating things, the easiest way to think of asynchronous is any operation that depends on something outside of the execution thread. If you need to read a file, or are waiting for a request to a server, then JavaScript will handle that separately from the thread your code is running.
So your program does not wait (by default) since there is nothing blocking it or preventing execution for continuing. This is a good thing, since blocking code (like endless loops) will make your application feel buggy and slow. You want your application to be responsive to user interaction while you are waiting on some external operation to run.
Enter Promises
Lets start with Promises (we will get to the application of promises shortly). Promises were introduced in the official ECMAScript 2015 standard. Appropriately named, Promises work as a way of informing a thread (or application instance) that a piece of code is probably doing some sort of async call or process, and will not be able to immediately return a value once it’s done executing.
To grossly over-simplify what happens, the Promise returns a special native object that will automatically resolve itself once it’s done executing. This does two things:
- Avoids direct entry into a place called callback hell, which causes very unpredictable and unreadable code. By default, this allows callback chaining (which we will cover shortly).
- Allows the management of Asynchronous code-bases (using
async await
syntax)
An example of a simple primitive Promise
const printNamesAsync = (firstName, lastName) =>
new Promise((resolve) => {
console.log(firstName, lastName);
resolve(true);
});
printNamesAsync('Flynn', 'B').then((result) => {
console.log('all names have been printed!', result);
});
console.log('hello');
/* outputs:
Flynn B
hello
all names have been printed! true
*/
We define a shorthand arrow function for a promise on line 1 that takes two arguments, firstName
and lastName
, which are then based to the promise initializer (on line 2).
On line 7, we actually execute the function that generates the promise. The interesting thing is that that operation is non-blocking, which means that the console.log
we did on line 11 is executed before the printNamesAsync
is finished executing.
On line 7 there is the usage of then
, which acts as an async callback that does not hold up the thread. It takes whatever values we pass to the resolve
call as the arguments.
This the basis for how promises work in JavaScript (very clear-cut from a development perspective).
Rejecting promises
Just like in real-life, Promises can be broken (and just as easily). Promises have a built in catch
chaining method that allows us to catch any rejected promises. To reject a promise, we simply need to add a second argument to our new Promise
statement:
const printNamesAsync = (firstName, lastName) =>
new Promise((resolve, reject) => reject());
printNamesAsync('Flynn', 'B')
.then(() => console.log('all names have been printed'))
.catch(() => console.log('we were unable to print the names'));
// 'we were unable to print the names'
Just like resolving a Promise, we are still able to execute a callback on the broken Promise. The only difference is that instead of using the then
, it will only use any chained catch
statements. In our above example, we have it setup on line 2 to immediately reject, thusly executing the catch
callback on line 5. The then
callback is skipped due to the Promise being broken.
Failing to add a
catch
callback to a Promise can result in an app-wide failure if an exception is thrown within. This includes any illegal exceptions, or any manualthrow
statements in your Promise.
Generators
At around the same time that Promises were coming into existence, there was also the introduction of co-routines (or generators), that allows for the execution of functions that can be executed multiple times (or return several values during it’s execution).
Example of a generator
function* generatorOfNames(firstName, lastName) {
yield firstName;
return lastName;
}
const nameGenerator = generatorOfNames('Mx', 'Generator');
let result = {};
while (!result.done) {
result = nameGenerator.next();
console.log(result);
}
console.log('Generator routine complete!');
/* outputs:
{ value: 'Mx', done: false }
{ value: 'Generator', done: true }
Generator routine complete!
*/
On line 1, we define a generator function with a special *****
character following the function
keyword to tell the JavaScript compiler that we are making a generator function.
On line 2, we yield the value of the firstName
argument. The yield
keyword is like return
, but it maintains an internal state that will continue execution when the iterator (generator) is called again. On line 3 we are finalizing the return of the generator (causing the generator to have no more remaining states)
On line 6, we are initializing a special function called an iterator, which will allow us to cycle through each of the yield
keywords and get each yielded value before it reaches the return
statement.
The block starting on line 8/9 is lopping through each value in the generator until iterator returns an object with { done: true }
Generators are mostly not used anymore (save some Node.js servers from a few years ago that use generators instead of Promises). It’s recommended that you spend more time getting to know Promises and async/await than spend too much time on generators.
For a great example of how to use generators (in async-event handling) would be (javascript-coroutines by Harold Cooper). We highly recommend you check it out if you are interested in learning more about how they work.
Async and Await
In the latest (and most secure) versions of Node.js (and thus V8 - the JavaScript engine used in Google Chrome and several WebKit Browsers), have access to some powerful syntax called async
and await
.
async
acts as a way to natively inform the JavaScript engine that a block of code is likely to be preforming asynchronous operations. Unlike promises however, we also then have access to the all-mighty await
syntax, which allows us to await for the execution of promises to complete before proceeding further along in the call.
An important thing to note is that native Promises and async functions are handled the same by the JavaScript engine with the exception being that async functions handle exceptions much more predictably (allowing you to wrap an
await
call in atry, catch
block).
An example of async/await in practice:
async function printNamesAsync(firstName, lastName) {
// some async operations...
return [firstName, lastName];
};
// An async IIFE to allow async/await calls
(async () => {
const names = await printNamesAsync('John', 'Appleseed');
console.log(names.join(' '));
// 'John Appleseed'
})();
The Async/Await calls only work within an async context, hence the use of an IIFE
In the above example, on line 1 we are defining an asynchronous function. It’s ultimately a function with the keyword async
added to the front. It resembles a standard JavaScript function, but counter-intuitively it returns a Promise
instead of a primitive value.
However, the await
on line 7 is telling the JavaScript engine to wait and get the resolved value of values of the async function, which in this case is the firstName
and lastName
passed to the call.
Promise.all
There is a powerful feature with promises that allow us to concurrently wait for several promises to resolve before proceeding. Promise.all
, is a static helper method that takes an Array as an argument, and allows the processing of several Promises. It also returns a promise, and only resolves when all Promises resolve without throwing.
async function printNamesAsync(firstName, lastName) {
// some async operations...
return [firstName, lastName];
};
// An async IIFE to allow async/await calls
(async () => {
const stack = [
printNamesAsync('John', 'Appleseed'),
printNamesAsync('Flynn', 'Buckingham'),
printNamesAsync('Joe', 'Smith'),
];
const names = await Promise.all(stack);
console.log(names.map(names => names.join(' ')));
// [ 'John Appleseed', 'Flynn Buckingham', 'Joe Smith' ]
})();
The above example closely resembles the original async/await
example above, save the fact that instead of awaiting a single call to printNamesAsync
, we have declared an Array of promises on line 8. On line 14, we await the entire stack of promises until all of theme have successfully resolve or had their errors handled.
On line 15, we use the Array map
method to concatenate the returned arrays to make a string version of the full names.
If any promises within a Promise.all
fail and if a catch
is not defined, the call will throw a rejection. Depending on your goals, you may or may not want to shotgun several Promises (attempt to resolve them if possible, and log if they can’t), or gracefully throw if any of the Promises fail.
Shotgunning Promises (hit-or-miss)
async function printNamesAsync(firstName, lastName) {
if (!firstName || !lastName) throw new Error(`invalid names "${firstName}" "${lastName}"`);
return [firstName, lastName];
};
(async () => {
const failed = [];
const onFailure = (error, names) => {
failed.push({ reason: error.message, names });
return [];
}
const params = [
['John', 'Appleseed'],
['Jason', null],
['Joe', 'Smith'],
];
const resolved = await Promise.all(params.map(names =>
printNamesAsync(...names).catch(onFailure)
));
console.log({ failed, resolved });
/* output:
{
failed: [ { reason: 'invalid names', names: [Array] } ],
resolved: [ [ 'John', 'Appleseed' ], [], [ 'Joe', 'Smith' ] ]
}
*/
})();
In the above example, we modified the printNamesAsync
function to throw (reject) it’s Promise when called with an invalid name. As a result, by default the Promise.all
would fail due to a single Promise not resolving.
But on line 7 to 11 we defined a custom array for storing failed attempts and an event handler for catching failed Promises. The event handler will return a blank []
Array as the Promise’s resolve on failure, (in case some other code attempts to iterate on it).
On line 13 to 17 we define an array of arguments to pass to the Promise.all
statement on line 19. On line 19, we map over each of the arguments present and convert it and its arguments to parameters via the spread ...
operator on line 20. On the same line, we also bind our onFailure
event callback to each promise inside the Promise.all
. This prevents the Promise.al
l from failing if a Promise inside rejects.
We are also using shorthand key-value parings on line 23. This is simply a away to save space when the key name and value of an object equates to a variable or reference of the same name.
Graceful Single-Failure
async function printNamesAsync(firstName, lastName) {
if (!firstName || !lastName) throw new Error(`invalid names "${firstName}" "${lastName}"`);
return [firstName, lastName];
};
(async () => {
const params = [
['John', 'Appleseed'],
['Jason', null],
['Joe', 'Smith'],
];
let failed = null;
const names = await Promise.all(params.map(names =>
printNamesAsync(...names)
)).catch((e) => {
console.error('The promise queue failed');
failed = e.message;
return [];
})
console.log({ names, failed });
/* outputs:
The promise queue failed
{ names: [], failed: 'invalid names "Flynn" "null"' }
*/
})();
In the above example, instead of attempting all of the promises (regardless of failures), it will attempt to resolve promises unless one of the names being processed has a Promise rejection. In that case, there is a catch
statement on line 15 that handles failures for the entire Promise.all
.
This is useful when a hard-failure is needed, while still allowing for a graceful way to report the error to the calling function or scope.
Conclusion
Async/await are very powerful tools for building and handling asynchronous events and operations, as unlike callback allow the utilization of Promises for pausing and resuming execution. Promises are able to take away many of the caveats of callbacks, while still providing a way to control the flow of your application.