25.9 C
New York
Friday, June 6, 2025

JavaScript promises: 4 gotchas and how to avoid them



I’ve previously covered the basics of JavaScript promises and how to use the async/await keywords to simplify your existing asynchronous code. This article is a more advanced look at JavaScript promises. We’ll explore four common ways promises trip up developers and tricks for resolving them.

Gotcha #1: Promise handlers return promises

If you’re returning information from a then or catch handler, it will always be wrapped in a promise, if it isn’t a promise already. So, you never need to write code like this:


firstAjaxCall.then(() => {
  return new Promise((resolve, reject) => {
	nextAjaxCall().then(() => resolve());
  });
});

Since nextAjaxCall also returns a promise, you can just do this instead:


firstAjaxCall.then(() => {
  return nextAjaxCall();
});

Additionally, if you’re returning a plain (non-promise) value, the handler will return a promise resolved to that value, so you can continue to call then on the results:


firstAjaxCall.then((response) => {
  return response.importantField
}).then((resolvedValue) => {
  // resolvedValue is the value of response.importantField returned above
  console.log(resolvedValue);
});


This is all very convenient, but what if you don’t know the state of an incoming value?

Trick #1: Use Promise.resolve() to resolve incoming values

If you are unsure if your incoming value is a promise already, you can simply use the static method Promise.resolve(). For example, if you get a variable that may or may not be a promise, simply pass it as an argument to Promise.resolve. If the variable is a promise, the method will return the promise; if the variable is a value, the method will return a promise resolved to the value:


let processInput = (maybePromise) => {
  let definitelyPromise = Promise.resolve(maybePromise);
  definitelyPromise.then(doSomeWork);
};

Gotcha #2: .then always takes a function

You’ve probably seen (and possibly written) promise code that looks something like this:


let getAllArticles = () => {
  return someAjax.get('/articles');
};
let getArticleById = (id) => {
  return someAjax.get(`/articles/${id}`);
};

getAllArticles().then(getArticleById(2));

The intent of the above code is to get all the articles first and then, when that’s done, get the Article with the ID of 2. While we might have wanted a sequential execution, what’s happening is these two promises are essentially being started at the same time, which means they could complete in any order.

The issue here is we’ve failed to adhere to one of the fundamental rules of JavaScript: that arguments to functions are always evaluated before being passed into the function. The .then is not receiving a function; it’s receiving the return value of getArticleById. This is because we’re calling getArticleById immediately with the parentheses operator.

There are a few ways to fix this.

Trick #1: Wrap the call in an arrow function

If you wanted your two functions processed sequentially, you could do something like this:


// A little arrow function is all you need

getAllArticles().then(() => getArticleById(2));

By wrapping the call to getArticleById in an arrow function, we provide .then with a function it can call when getAllArticles() has resolved.

Trick #2: Pass in named functions to .then

You don’t always have to use inline anonymous functions as arguments to .then. You can easily assign a function to a variable and pass the reference to that function to .then instead.


// function definitions from Gotcha #2
let getArticle2 = () => {
  return getArticleById(2);
};

getAllArticles().then(getArticle2);


getAllArticles().then(getArticle2);

In this case, we are just passing in the reference to the function and not calling it.

Trick #3: Use async/await

Another way to make the order of events more clear is to use the async/await keywords:


async function getSequentially() {
  const allArticles = await getAllArticles(); // Wait for first call
  const specificArticle = await getArticleById(2); // Then wait for second
  // ... use specificArticle
}

Now, the fact that we take two steps, each following the other, is explicit and obvious. We don’t proceed with execution until both are finished. This is an excellent illustration of the clarity await provides when consuming promises.

Gotcha #3: Non-functional .then arguments

Now let’s take Gotcha #2 and add a little extra processing to the end of the chain:


let getAllArticles = () => {
  return someAjax.get('/articles');
};
let getArticleById = (id) => {
  return someAjax.get(`/articles/${id}`);
};

getAllArticles().then(getArticleById(2)).then((article2) => { 
  // Do something with article2 
});

We already know that this chain won’t run sequentially as we want it to, but now we’ve uncovered some quirky behavior in Promiseland. What do you think is the value of article2 in the last .then?

Since we’re not passing a function into the first argument of .then, JavaScript passes in the initial promise with its resolved value, so the value of article2 is whatever getAllArticles() has resolved to. If you have a long chain of .then methods and some of your handlers are getting values from earlier .then methods, make sure you’re actually passing in functions to .then.

Trick #1: Pass in named functions with formal parameters

One way to handle this is to pass in named functions that define a single formal parameter (i.e., take one argument). This allows us to create some generic functions that we can use within a chain of .then methods or outside the chain.

Let’s say we have a function, getFirstArticle, that makes an API call to get the newest article in a set and resolves to an article object with properties like ID, title, and publication date. Then say we have another function, getCommentsForArticleId, that takes an article ID and makes an API call to get all the comments associated with that article.

Now, all we need to connect the two functions is to get from the resolution value of the first function (an article object) to the expected argument value of the second function (an article ID). We could use an anonymous inline function for this purpose:


getFirstArticle().then((article) => {
  return getCommentsForArticleId(article.id);
});

Or, we could create a simple function that takes an article, returns the ID, and chains everything together with .then:


let extractId = (article) => article.id;
getFirstArticle().then(extractId).then(getCommentsForArticleId);

This second solution somewhat obscures the resolution value of each function, since they’re not defined inline. But, on the other hand, it creates some flexible functions that we could likely reuse. Notice, also, that we’re using what we learned from the first gotcha: Although extractId doesn’t return a promise, .then will wrap its return value in a promise, which lets us call .then again.

Trick #2: Use async/await

Once again, async/await can come to the rescue by making things more obvious:


async function getArticleAndComments() {
  const article = await getFirstArticle();
  const comments = await getCommentsForArticleId(article.id); // Extract ID directly
  // ... use comments
}

Here, we simply wait for getFirstArticle() to finish, then use the article to get the ID. We can do this because we know for sure that the article was resolved by the underlying operation.

Gotcha #4: When async/await spoils your concurrency

Let’s say you want to initiate several asynchronous operations at once, so you put them in a loop and use await:


// (Bad practice below!)
async function getMultipleUsersSequentially(userIds) {
  const users = [];
  const startTime = Date.now();
  for (const id of userIds) {
    // await pauses the *entire loop* for each fetch
    const user = await fetchUserDataPromise(id); 
    users.push(user);
  }
  const endTime = Date.now();
  console.log(`Sequential fetch took ${endTime - startTime}ms`);
  return users;
}
// If each fetch takes 1.5s, 3 fetches would take ~4.5s total.

In this example, what we want is to send all these fetchUserDataPromise() requests together. But what we get is each one occurring sequentially, meaning the loop waits for each to complete before continuing to the next.

Trick #1: Use Promise.all

Solving this one is simple with Promise.all:


// (Requests happen concurrently)
async function getMultipleUsersConcurrently(userIds) {
  console.log("Starting concurrent fetch...");
  const startTime = Date.now();
  const promises = userIds.map(id => fetchUserDataPromise(id));

  const users = await Promise.all(promises);

  const endTime = Date.now();
  console.log(`Concurrent fetch took ${endTime - startTime}ms`);
  return users;
}
// If each fetch takes 1.5s, 3 concurrent fetches would take ~1.5s total (plus a tiny overhead).

Promise.all says to take all the Promises in the array and start them at once, then wait until they’ve all completed before continuing. In this use case, promises are the simpler approach than async/await. (But notice we’re still using await to wait for Promise.all to complete.)

Conclusion

Although we often can use async/await to resolve issues in promises, it’s critical to understand promises themselves in order to really understand what the async/await keywords are doing. The gotchas are intended to help you better understand how promises work and how to use them effectively in your code.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles