Easy async/await in JavaScript
A not-so-long time ago, JavaScript was renowned for a thing called callback hell. I should know, I've written projects which would have nested levels of callbacks. For example:
function saveWebPage(url, callback) {
// Get the web page
getWebPage(url, function(err, html) {
if (err) callback(err);
// Load it if exists
loadPage(url, function(err, page) {
if (err) callback(err);
if (page === null) {
// Insert it
insertPage(url, html, function (err) {
if (err) callback(err);
callback(null, html);
})
} else {
// Update it
updatePage(url, html, function (err) {
if (err) callback(err);
callback(null, html);
}
}
});
});
}
Yuk 🤮! It would be messy. And worst part was, if you needed do to an if
statement, you had to do one of two things. Create functions for each branch, or duplicate code inside the if
statement. The cognitive overhead was too much.
Introducing Promises
Then Promises came along. They were a neat way to chain things together. But they weren't exactly the easiest to remember how to write. The above example would be written something like this:
function saveWebPage(url) {
var pageHtml = null;
return new Promise(function (resolve, reject) {
getWebPage(url).then(function (html) {
pageHtml = html;
return loadPage(url);
}).then(function (page) {
if (page === null) {
return insertPage(url, pageHtml);
} else {
reutrn updatePage(url, pageHtml);
}
}).then(resolve)
.catch(reject);
});
}
Using Promises, you can simplify your work by returning a Promise
object that will either resolve or reject. Resolve means the work was successful and it has a value to return, and reject means there was an error.
In our example, we have chained everything together. It's one step away from callback hell that we had in our first example.
But honestly, I find this very cumbersome to write and can never really remember how to chain, when to do a new Promise
and how to return values from one then
call to the next.
Async/Await
JavaScript introduced two new keywords async
and await
which extends the behaviour of Promises but without having to write the full verbose Promise code.
const saveWebPage = async (url) {
const html = await getWebPage(url);
const page = await loadPage(url);
if (page === null) {
await insertPage(url, html);
} else {
await updatePage(url, html);
}
}
Under the covers, the await
keyword simply blocks the current JavaScript execution context on a Promise and waits for it to either resolve or reject. Unlike other blocking in JavaScript (like calling fs.readFileSync
), other things can still run, such as other callbacks listening to network ports, file reading, database calls, etc.
It's a great way to simulate a synchronous threads in an event driven programming language, still without using threads like in Java.
How to call an async method
You may find yourself needing a simple script to run that uses await
. But you can't use await
if it's not inside an async
method. How do we run an async
method? Here's how:
import { saveWebPage } from "./mylib.js";
const url = process.argv[0];
console.log('Saving ' + url);
(async () => {
try {
await saveWebPage(url);
console.log("Web page saved");
} catch (ex) {
console.error("Unable to save web page:", ex);
}
})(); // Call our async method immediately
Basically we've wrapped our async
method in brackets and called it immediately. Now we are free to use await
🕺.
async/await with classes
You may have a situation where you have a class
and you want to have async methods on it. You can do something like this:
class WebPageDownloader {
async getWebPage(url) {
return await getWebPage(url);
}
}
(async () => {
const downloader = new WebPageDownloader();
const html = await downloader.getWebPage('https://zaro.io');
console.log('Downloaded', html);
})();
The above demonstrates that you can add an async
keyword to a class method and await on it. You can also add it to static methods as well:
class WebPageDownloader {
static async getWebPage(url) {
return await getWebPage(url);
}
}
await WebPageDownloader::getWebpage('https://zaro.io');
await on any Promise
One neat trick is that you can await
on any Promise
object. For example:
const sleepFor60Seconds = new Promise((resolve, reject) => {
setTimeout(resolve, 60000);
});
await sleepFor60Seconds;
Which means that async
IS a Promise
object 🤯!
async IS a promise
The magic piece of information is that when you have a function that is an async
function, this is just shorthand for it being wrapped in a new Promise
and the return value is passed to the resolve
and the exceptions are passed to the reject
. This means you can mix and match the use of promises and async methods.
For example:
// Await on Promise object
const promise1 = new Promise((resolve, reject) => {resolve('success'});
await promise1;
// Await on Async function (which is a promise object)
const promise2 = async () => {return 'success'};
await promise2;
Notice that await
waits on a promise object, not on a function? This means that when we use it for a function, two things are happening. Firstly, we're executing the async
function immediately which only generates a Promise
object. Then it waits on the promise object.
const result = await getResult();
// Is the same as
const promise = getResult();
await promise;
Older browsers
Async functions where only introduced in Microsoft Edge and IE11 does not support it. There is generally good support across other browsers. The good news is, you can compile this to older versions of JavaScript using Babel. But that is outside the scope of this article.
In summary
Since using async/await, I now use JavaScript and Node.js for almost all forms of scripting. Previously I would use PHP because it is synchronous by nature and everything blocks. Using Node.js in the early days was cumbersome and had callback hell. But now it's quick and elegant to write code that is very expressive with minimal code.