Stackify is now BMC. Read theBlog

Three Async JavaScript Approaches

By: Cory
  |  May 2, 2023
Three Async JavaScript Approaches

There are multiple ways to handle asynchronous code in JavaScript today. In this post, we’ll explore the three most popular async options by making an HTTP call to the jsonplaceholder API.

Callback Functions

Callbacks are conceptually simple — you pass a function as a parameter that should be called later (when the async code completes).
Call me, maybe.

Functions are first-class in JavaScript. This means you can pass functions around as arguments, and return functions from functions. Handy. When JavaScript was first released, callbacks were the standard approach for handling asynchronous operations. Callbacks are conceptually simple — you pass a function as a parameter that should be called later (when the async code completes).

In this example, I’m passing a callback to getPosts. Inside getPosts, I’m using XMLHttpRequest to make an HTTP call.

getPosts(function(error, posts) {
  if (error) {
  } else {

function getPosts(callback) {
    const xhr = new XMLHttpRequest();
    xhr.responseType = 'json';
    xhr.onreadystatechange = function() {
        if (xhr.readyState == XMLHttpRequest.DONE) {
            callback(null, xhr.response);
    }"GET", "");
    try {
    } catch (error) {

I’m using Node’s callback style above, which involves specifying an error handler for the first parameter of the callback function (line 1). When the HTTP request completes, the callback is called on line 14. If an error occurs, an error is passed to the callback on line 21.


  1. Simple — Callbacks are conceptually simple. You pass a function that you want to run later.
  2. Universal — Callbacks run everywhere. No transpilation or polyfills required.


  1. Clunky composition — They don’t compose as elegantly as alternatives below. Nested callbacks can lead to deeply nested code — commonly called callback hell, though this concern can be mitigated by extracting code to separate functions.
  2. Clunky error handling — As you can see above, you have to pass the error around rather than using traditional try/catch.
  3. Unintuitive flow — Callbacks require you to jump around to comprehend the code’s flow. Alternative patterns below provide a more linear reading experience.


A promise is an object that represents the eventual completion of an async operation, and its resulting value.
Promise: A guarantee to deliver later.

A promise is an object that represents the eventual completion of an async operation, and its resulting value. Today, promises are the most popular asynchronous approach. Promises are also the foundation for async/await (discussed below), so they remain well worth mastering.

Many libraries offer promise-based APIs such as Axios for HTTP calls, and native browser fetch. In this example, I’m using fetch to make an HTTP call instead of XMLHttpRequest because fetch provides a simple promise-based API for making asynchronous HTTP calls. Fetch is built into modern browsers.

getPosts().then( posts => {

function getPosts() {
  return fetch("")
  .then(response => {
    return response.json();
  .catch( error => console.log(error));

By convention, you handle a completed promise within a function called then(line 7). So, I return the promise that fetch supplies on line 6, and handle the resolved promise inside then on line 8. Finally, I handle any errors using .catch, also part of the promise spec.


  1. Easily chainable — Promises can be easily chained together to handle complex asynchronous flows without resorting to the deep nesting required with callbacks.
  2. Powerful — Promises provide exceptional power for composing complex asynchronous operations. You can utilize whichever promise resolves first via promise.race, and run multiple async operations simultaneously with promise.all.


  1. Swallowed exceptions — You must declare .catch for error handling instead of traditional try/catch. Without a .catch, some browsers silently swallow unhandled exceptions in promises.
  2. Error prone API — You must be sure to return the promise — note the return on line 6 above. Without it, you’ll get a confusing error: Cannot read property ‘then’ of undefined. Promises are powerful, but they’re easy to get wrong.
  3. Polyfill required — Promises aren’t supported in Internet Explorer, so be sure to use a promise polyfill.


Async/await is a new approach that was added to JavaScript in ES2017
Await your turn.

Async/await is a new approach that was added to JavaScript in ES2017. It actually uses promises behind the scenes, so you can think of async/await as syntactic sugar over promises. Thus, you still need to understand promises to use async/await. Like promises, async/await is non-blocking.

Async/await is often preferable over promises and callbacks because it makes async code look more like synchronous code:

etPosts().then(posts => {

async function getPosts() {
  try {
    const response = await fetch("");
    return response.json();
  } catch(error) {

Asynchronous functions are declared with the async keyword (line 5). On line 7, I declare an async operation using the await keyword. The function will pause at the await keyword until the async call completes.

Async functions return a promise, so you still use promises to handle the response, as you can see on line 1 above. And you don’t use the await keyword on the return statement on line 8 because an async function’s return value is automatically wrapped in promise.resolve.

Recent versions of Node have async/await support built-in. Modern browsers support it too, but you’ll want to transpile using something like Babel or TypeScript for Internet Explorer support.


  1. Traditional try/catch — Unlike promises, Async/await allows you to use traditional try/catch for error handling.
  2. Easier to read — Nesting is reduced via the await keyword. With callbacks and promises, multiple async operations can lead to code that’s harder to read. With async/await, you just add another await keyword. No extra nesting required.
  3. Easy debugging — When you’ve chained multiple async operations together, async/await produces a more useful stack trace because it displays the exact await statement that failed. With chained promises, the error message is often ambiguous. It’s also straightforward to set a breakpoint on an async statement. In contrast, with some promise structures, you’ll need to refactor just to be able to set a breakpoint.

For more examples of the advantages above, read here.


  1. Less power  — You lose some compositional power with async/await. For example, With async/await, each await keyword will pause, so there’s no way to run multiple async operations simultaneously. In contrast, with promises, you can run multiple async operations simultaneously using promise.all.
  2. Transpile bloat — If you need to transpile for your environment, the resulting code is bloated. For example, with Babel, 8 lines becomes 37 lines. However, some of this bloat is a one-time cost. If you use async/await multiple times, the code in _asyncToGenerator is reused.

[adinserter block=”33″]

Other Async Options

One can also use raw iterators and generators, or interesting alternative libraries like RxJS, but promises and async/await are sufficiently powerful for most use cases.


Modern async approaches like Async/await are handy, but you still need to understand promises to use them. Favor async/await over promises and callbacks since the result is typically easier to read and maintain.

Want to toy around with this more? Here’s a GitHub repo you can clone and run locally.

Need to monitor your javascript applications? Use Stackify Retrace and its real user monitoring capabilities.

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]