Converting Promises to Async Await in JavaScript

Learn how to refactor promise chains into async/await syntax step by step. See side-by-side conversions, handle errors with try...catch, and avoid common conversion pitfalls.

7 min read

Converting promises to async/await means rewriting .then() callbacks as sequential await statements inside an async function. The runtime behavior stays the same. The code becomes linear: you read it top to bottom instead of following a chain of callbacks.

Every promise chain can be converted, and every async/await function can be converted back to promises. They are two syntaxes for the same underlying mechanism.

javascriptjavascript
// Promise chain
function getUserEmail(id) {
  return fetch(`/users/${id}`)
    .then((res) => res.json())
    .then((user) => user.email);
}

The async/await version below does the same fetch and the same two-step unwrap, but each intermediate value gets its own line and its own name instead of living inside a callback parameter.

javascriptjavascript
// async/await equivalent
async function getUserEmail(id) {
  const res = await fetch(`/users/${id}`);
  const user = await res.json();
  return user.email;
}

Both functions return a promise that resolves to the user's email. The second version reads like synchronous code, with each step on its own line and the intermediate res and user values available as named variables.

The mechanical conversion rules

Converting a promise chain to async/await follows a small set of rules that apply the same way every time, no matter how the chain is shaped.

Promise patternasync/await equivalent
then callbackconst result = await promise, then the body
catch callbacktry block, then catch (err)
finally callbacktry block, then finally
Returning a value from thenReturn the value directly
Returning a promise from thenreturn await promise, or just return promise

The most important rule is that whatever was inside a then callback body moves directly after the matching await line, and the callback's parameter name becomes a plain variable.

javascriptjavascript
// Before: promise chain
getUser(1)
  .then((user) => {
    console.log(`Processing ${user.name}`);
    return getPosts(user.id);
  })
  .then((posts) => {
    console.log(`Found ${posts.length} posts`);
    return posts.filter((p) => p.published);
  });

The converted version below keeps the exact same two log statements and the same filter call, just written as sequential steps instead of chained callbacks.

javascriptjavascript
// After: async/await
async function processUser() {
  const user = await getUser(1);
  console.log(`Processing ${user.name}`);
 
  const posts = await getPosts(user.id);
  console.log(`Found ${posts.length} posts`);
 
  return posts.filter((p) => p.published);
}

Notice that the console.log calls stay exactly where they were. The only thing that changes is how values move between steps, from chain parameters to ordinary variables.

Converting error handling

Promise chains use .catch() at different positions for different error-handling strategies. Each catch becomes a try...catch at the same logical position in the async version.

javascriptjavascript
// Before: chain with mid-chain error recovery
fetchUser(id)
  .then((user) => processUser(user))
  .catch((err) => {
    console.warn("Processing failed, using fallback:", err.message);
    return fallbackUser;
  })
  .then((user) => saveUser(user));

The try block below wraps exactly the two steps that the catch covered in the chain above, and the catch block provides the same fallback value.

javascriptjavascript
// After: async/await with equivalent try/catch
async function handleUser(id) {
  let user;
 
  try {
    user = await fetchUser(id);
    user = await processUser(user);
  } catch (err) {
    console.warn("Processing failed, using fallback:", err.message);
    user = fallbackUser;
  }
 
  return saveUser(user);
}

Execution continues with saveUser regardless of which path the try...catch took, matching how the original chain always reached its final then call.

A final catch at the end of a chain becomes a try...catch around the whole async function body, or you can let the caller handle it since async functions already return promises.

javascriptjavascript
// Before: error handling at the call site
fetchData().then(render).catch(showError);
 
// After: still handle at the call site
async function main() {
  try {
    const data = await fetchData();
    render(data);
  } catch (err) {
    showError(err);
  }
}

Handling Promise.all and other combinators

Promise.all and similar combinators already return promises, so they slot into an async function without any restructuring. You still call them the same way, only now with await in front instead of a then.

javascriptjavascript
// Before: Promise.all in a chain
function loadPage() {
  return Promise.all([fetchUser(1), fetchPosts(1), fetchComments(1)])
    .then(([user, posts, comments]) => {
      return { user, posts, comments };
    });
}

The array destructuring that used to live inside the then callback parameter moves to the left side of the await instead.

javascriptjavascript
// After: destructure the awaited result
async function loadPage() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(1),
    fetchPosts(1),
    fetchComments(1),
  ]);
 
  return { user, posts, comments };
}

The parallel execution is preserved exactly. Promise.all still starts all three fetches at once, and await simply waits for all of them together. Converting to await here does not turn the parallel calls into sequential ones.

The array literal passed to Promise.all still creates every promise before any of them is awaited, so nothing about the timing changes just because the syntax looks more linear. For more on parallel patterns, see JavaScript Async Await Guide: Complete Tutorial.

Handling Promise.race and Promise.any

Promise combinators like race and any need no conversion at all, because they already return a promise on their own. Wrapping the call in an async function and awaiting or returning it works exactly the same as before.

javascriptjavascript
function fetchWithTimeout(url, ms) {
  return Promise.race([
    fetch(url),
    new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), ms)),
  ]);
}

This function works identically whether the surrounding code calls it with .then() or awaits it inside another async function, since fetchWithTimeout itself never needed to change.

The lesson here is not to hunt for a conversion step that does not exist. If a function already returns a promise directly, without a .then() chain wrapped around it, there is nothing left to rewrite. For more on this pattern, see How to Use Promise Race in JS: Complete Guide.

Common conversion mistakes

Forgetting to make the function async. Using await inside a function that is not marked async throws a syntax error, since await is only meaningful inside an async function.

javascriptjavascript
// Mistake: missing async
function getData() {
  const res = await fetch("/data"); // SyntaxError
}
 
// Correct
async function getData() {
  const res = await fetch("/data");
}

Leaving then callbacks half-converted. A common halfway state mixes await at the top of a function with a leftover then further down. Either finish the conversion or leave the original chain intact.

javascriptjavascript
// Confusing half-conversion
async function loadStuff() {
  const user = await fetchUser(1);
  return getPosts(user.id).then((posts) => posts.length);
}
 
// Clean: finish the conversion
async function loadStuff() {
  const user = await fetchUser(1);
  const posts = await getPosts(user.id);
  return posts.length;
}

Shadowing variables between chain steps. In a promise chain, each then callback creates its own scope. In async/await, every variable shares the function's scope, so reusing the same name for two different steps causes the second one to overwrite the first.

javascriptjavascript
// Promise chain: each then has its own scoped result
fetchA()
  .then((result) => transformA(result))
  .then((result) => transformB(result)); // Different result

The async/await version needs distinct names for each step since there is no separate scope to keep them apart automatically.

javascriptjavascript
// async/await: use distinct names
async function process() {
  const resultA = await fetchA();
  const transformedA = transformA(resultA);
  const resultB = await fetchB(transformedA);
  return transformB(resultB);
}

Using await inside a non-async callback. Await only works directly inside async functions. It does not work inside callbacks passed to .map(), .forEach(), or event handlers unless those callbacks are marked async too.

javascriptjavascript
// Mistake: await inside .map() callback (not async)
const slugs = urls.map((url) => {
  const res = await fetch(url); // SyntaxError
});

The fix marks the callback itself as async, then wraps the whole array of promises in Promise.all so the outer function can await them together.

javascriptjavascript
// Correct: make the callback async, then use Promise.all
const slugs = await Promise.all(
  urls.map(async (url) => {
    const res = await fetch(url);
    return res.url;
  })
);

When to convert and when to keep promises

Convert to async/await whenKeep promise chains when
The chain has 3 or more stepsThe chain is one or two then calls
You need to name intermediate valuesValues flow naturally without names
Error handling needs try...catchA single catch at the end is enough
You have conditional logic between stepsEach step is a single expression
The chain is hard to readThe chain is already clear

Neither style is better in absolute terms. Choose the one that makes the intent clearest. A short two-step chain is often cleaner left as-is than wrapped in an async function with two await lines, but the moment a step needs a conditional, async/await becomes the clearer choice.

For the promise chaining fundamentals, see JavaScript Promise Chaining: A Complete Guide.

Rune AI

Rune AI

Key Insights

  • Every .then(callback) becomes const result = await promise; followed by the callback body.
  • Replace .catch() with try...catch blocks at the same position in the flow.
  • async/await makes intermediate values available as named variables instead of chain parameters.
  • Promise.all() works naturally with await: just await the result.
  • Avoid mixing .then() and await in the same function without a clear reason.
RunePowered by Rune AI

Frequently Asked Questions

Can every promise chain be converted to async/await?

Yes. async/await is syntactic sugar over promises. Any .then() chain can be rewritten as an async function with await. The reverse is also true.

Should I always use async/await instead of promises?

No. Short chains with a single .then() are often cleaner as-is. async/await shines when you have multiple steps, need intermediate variable names, or want try...catch for error handling.

What happens to .catch() when I convert to async/await?

Replace .catch() with try...catch inside the async function. If you had multiple .catch() calls at different points in the chain, use a try...catch at each corresponding position.

Conclusion

Converting a promise chain to async/await is a mechanical process. Find each .then(), move its callback body after an await, assign the result to a variable instead of chaining, and wrap error paths in try...catch. The runtime behavior is identical. The difference is in how the code reads.