9/18 | Validate feature ideas earlier with AI-driven prototypes

What are best AI tools? Take the State of AI survey

Builder.io
Builder.io
Contact sales

9/18 | Validate feature ideas earlier with AI-driven prototypes

What are best AI tools? Take the State of AI survey

Builder.io
Builder.io
< Back to blog

web development

Avoiding Async/Await Footguns

March 6, 2023

Written By Yoav Ganbar

Async / await was a breath of fresh air for those who did web development prior to 2014. It simplified the way you write asynchronous code in JavaScript immensely.

However, as with everything in the JS world and programming, it does have a few gotchas that can be a problem later.

But before we get down to business and give you those sweet tips you crave to avoid shooting yourself in the foot, I thought it’d be nice to first take a little trip down memory lane.

The dark ages (pre-ES6 era)

Depending on how long you have been in web development, you may have seen the evolution of handling async operations in JavaScript.

Once upon a time, before the days of arrow functions and Promises, prior to ECMAScript 6 (2015), JS devs had only callback functions to deal with asynchronous code.

So, writing code “the old way” would look like the example below.

From a glance, this might seem somewhat manageable, but this lead to “beautiful” code like the below that you need Ryu from Street Fighter to indent (props to reibitto for creating Hadoukenify).

That is what you call “callback hell”.

Promises to the rescue

To escape this, ES6 hit the scene with the mighty Promise web API.

an illustration of a Promise life cycle Source: MDN

The idea is that an async task has a lifecycle consisting of 3 states: pending, fulfilled (also called resolved), or rejected.

Furthermore, this came with a novel concept - then() which allowed us to do something with a resolved promise value.

But not only.

This also allowed returning a promise as the resolved value which in turn can also have a then() method, which ultimately enabled promise chaining.

The gist is that you can pipe a returned value from one async operation to the next.

A great step forward with great power for sure!

However, as good ol’ Uncle Ben told Peter Parker “with great power…” - I’m assuming you know the rest of the phrase 😉.

So, please don’t do this:

a code screenshot of a long nested async function with many “then” functions.

Async / Await FTW

Promises are great, but as we’ve learned from the above, they can be poorly written.

What more is that even though JavaScript is async by nature, a lot of its APIs are synchronous and are much easier to reason about.

Reading top to bottom is more natural to us as humans, and even more so, reading code the same way helps maintain the order of execution of a program more clear.

So now, if we take the code from the pre-ES6 era, we can refactor it using both Promises and async / await:

The thing to note here is that we need to have an async function in order to use the await keyword.

Overall, it’s just much more readable. We wait for our data in one line, then we wait to process it, and finally, we log it.

Clean, and without all that nesting and indentation.

But as I’ve mentioned, it can sometimes lead to unexpected footguns.

Let’s dive in.

Not Handling Errors Correctly

One of the most common footguns when using async/await is not handling errors correctly. When using await, if the awaited promise rejects, an error will be thrown.

Swallowed code paths

As mentioned, the Promise instance has a reject method, which might help handle that situation.

You might think that if write your promise like the snippet below, that should do it.

Then you’d use it like so:

However, if we simulate an error and run about the same code, for example:

Nothing will be logged to the console, besides our rejected promise:

Screenshot of devtools showing only reject promise error.

Our logs were swallowed by the promise rejection 😱.

Try / catch is better

A common way to handle this would be to wrap our await statements in a try...catch block.

But guess what? There’s a footgun here.

Let’s observe how this approach can fail.

Assume every awaited statement can reject

In case you have a flow that has multiple awaits think about creating alternative sub-flows. As David Khourshid wrote a while back ago: “Async/await is too convenient sometimes”:

Let’s use his contrived example to illustrate this:

Now with error handling and multiple try...catch blocks:

The point is that you should have try...catch blocks at the appropriate boundaries for your use cases.

The .catch() preference

Some like to just write:

It's a good way to go as well. Allows using const instead of let and read the code flow a bit nicer.

Third-party ergonomics

There are also libraries out there that can assist with handling errors as well.

I recently learned about fAwait, which approaches in a more functional way of writing. It adds some utility functions and a way to pipe errors into the next "layer" and add side effects. Kind of like middlewares.

Instead of code like this:

You can write it like this:

I haven't tried it yet, but it looks interesting.

Over chaining when you don’t need to

Another footgun is having too many sequential awaits, similar to the previous example.

The caveat is that the await keyword, as I’ve mentioned, makes async code execute in a synchronous fashion. That is, as implied from the keyword, each line waits for a promise to be resolved or rejected before moving to the next line.

There’s a way to make promises execute concurrently, which is by using either Promise.all() or Promise.allSetteled():

For a deeper dive into this topic, read Steve’s post.

Conclusion

In conclusion, async / await is a powerful tool for simplifying asynchronous code in JavaScript, but it does come with some footguns. To avoid common pitfalls, it's important to handle errors correctly, not overuse async/await, and understand execution order. By following these tips, you'll be able to write more reliable asynchronous code that won't break later.

About me

Hi! I’m Yoav, I do DevRel and Developer Experience at Builder.io.

We make a way to drag + drop with your components to create pages and other CMS content on your site or app, visually.

You can read more about how this can improve your workflow here.

You may find it interesting and useful!

Share

Twitter
LinkedIn
Facebook
Share this blog
Copy icon
Twitter "X" icon
LinkedIn icon
Facebook icon

Visually edit your codebase with AI

Using simple prompts or Figma-like controls.

Try it nowGet a demo

Design to Code Automation

A pragmatic guide for engineering leaders and development teams


Continue Reading
design9 MIN
How to generate (actually good) designs with AI
September 17, 2025
AI9 MIN
7 Levels of Context Engineering for Designers
September 16, 2025
Design to Code8 MIN
Git Branching for Designers
September 11, 2025

Product

Visual CMS

Theme Studio for Shopify

Sign up

Login

Featured Integrations

React

Angular

Next.js

Gatsby

Resources

User Guides

Developer Docs

Forum

Blog

Github

Get In Touch

Chat With Us

Twitter

Linkedin

Careers

© 2020 Builder.io, Inc.

Security

Privacy Policy

Terms of Service

Get the latest from Builder.io

By submitting, you agree to our Privacy Policy

  • Fusion

  • Publish

  • Product Updates

  • Design to Code

  • Headless CMS

    Multi-Brand CMS

  • Landing Pages

  • Web Apps

  • Prototypes

  • Marketing Sites

  • Headless Commerce

  • Documentation

  • Fusion Docs

  • Publish Docs

  • Blog

  • Webinars

  • Guides

  • Case Studies

  • Community Forum

  • Partners

  • Affiliate Program

  • CMS Integrations

  • CMS Blueprints

  • Glossary

  • Figma to Code Guide

  • Headless CMS Guide

  • Headless Commerce Guide

  • Composable DXP Guide

  • About

  • Careers

  • Contact Sales

Security

Privacy Policy

SaaS Terms

Compliance

Cookie Preferences

YouTube icon
Github icon
Blsky Icon
Twitter "X" icon
LinkedIn icon
Feed Icon
Gartner Cool Vendor 2024