Converting Promises to async/await in JavaScript

Learn step-by-step how to convert JavaScript Promise chains to async/await syntax. See before/after refactoring examples, handling .catch() recovery, parallel operations, and wrapping legacy callback APIs.

JavaScriptintermediate
11 min read

Migrating from .then() chains to async/await is one of the most common JavaScript refactoring tasks. The two styles are interchangeable because async/await is built on top of Promises, but async/await produces more readable code for most sequential patterns. This guide provides systematic before/after conversions for every common scenario.

The Core Conversion Rule

The mechanical translation is:

  • .then(value => ...) becomes const value = await ...
  • .catch(err => ...) becomes try { ... } catch (err) { ... }
  • Returning a value in .then() becomes returning a value directly
  • Returning a Promise in .then() becomes awaiting that Promise
javascriptjavascript
// Promise chain
function getUser(id) {
  return fetchUser(id)
    .then((user) => user.name.toUpperCase());
}
 
// async/await equivalent
async function getUser(id) {
  const user = await fetchUser(id);
  return user.name.toUpperCase();
}

Conversion 1: Simple Chain

javascriptjavascript
// BEFORE: Promise chain
function loadPage(slug) {
  return fetchArticle(slug)
    .then((article) => {
      return fetchAuthor(article.authorId);
    })
    .then((author) => {
      return renderPage(author);
    });
}
 
// AFTER: async/await
async function loadPage(slug) {
  const article = await fetchArticle(slug);
  const author = await fetchAuthor(article.authorId);
  return renderPage(author);
}

Notice renderPage is called directly with return — if it is synchronous (or if you want to return its Promise), no await is needed on the final step (though adding it is also fine).

Conversion 2: Error Handling With .catch()

javascriptjavascript
// BEFORE: .catch() at end of chain
function loadUser(id) {
  return fetchUser(id)
    .then((user) => processUser(user))
    .catch((err) => {
      console.error(err.message);
      return null;
    });
}
 
// AFTER: try/catch wrapping
async function loadUser(id) {
  try {
    const user = await fetchUser(id);
    return processUser(user);
  } catch (err) {
    console.error(err.message);
    return null;
  }
}

Conversion 3: Mid-Chain Recovery (.catch() Followed by .then())

This is where async/await requires more care. A .catch() in the middle of a chain that recovers and continues must be expressed differently:

javascriptjavascript
// BEFORE: mid-chain recovery
function loadWithFallback(id) {
  return fetchPrimary(id)
    .catch(() => fetchFallback(id))   // Recovery
    .then((data) => processData(data)); // Continues after recovery
}
 
// AFTER: option 1 — nested try/catch
async function loadWithFallback(id) {
  let data;
  try {
    data = await fetchPrimary(id);
  } catch {
    data = await fetchFallback(id);    // Recovery
  }
  return processData(data);           // Continues after recovery
}
 
// AFTER: option 2 — keep .catch() inline for recovery
async function loadWithFallback(id) {
  const data = await fetchPrimary(id).catch(() => fetchFallback(id));
  return processData(data);
}
// Option 2 mixes styles but is concise and readable

Conversion 4: .finally()

javascriptjavascript
// BEFORE: .finally() for cleanup
function fetchWithSpinner(url) {
  showSpinner();
  return fetch(url)
    .then((r) => r.json())
    .catch((err) => { notifyError(err); throw err; })
    .finally(() => hideSpinner());
}
 
// AFTER: try/catch/finally
async function fetchWithSpinner(url) {
  showSpinner();
  try {
    const response = await fetch(url);
    return await response.json();
  } catch (err) {
    notifyError(err);
    throw err;
  } finally {
    hideSpinner(); // Always runs
  }
}

JavaScript's finally block in async functions behaves identically to .finally() — it runs regardless of outcome and does not change the return value (unless it throws).

Conversion 5: Parallel Operations (Promise.all)

Do NOT naively convert .then() chains to sequential awaits when operations are independent. This is the most important performance consideration:

javascriptjavascript
// BEFORE: Promise.all for parallel execution
function loadDashboard(userId) {
  return Promise.all([
    fetchUser(userId),
    fetchPosts(userId),
    fetchStats(userId),
  ]).then(([user, posts, stats]) => ({
    user,
    posts,
    stats,
  }));
}
 
// AFTER: keep Promise.all, just await it
async function loadDashboard(userId) {
  const [user, posts, stats] = await Promise.all([
    fetchUser(userId),
    fetchPosts(userId),
    fetchStats(userId),
  ]);
  return { user, posts, stats };
}
 
// WRONG conversion (makes everything sequential):
async function loadDashboardWrong(userId) {
  const user = await fetchUser(userId);   // 200ms
  const posts = await fetchPosts(userId); // 300ms (waits for user fetch)
  const stats = await fetchStats(userId); // 150ms (waits for posts fetch)
  // Total: 650ms instead of 300ms!
  return { user, posts, stats };
}

Conversion 6: Returning From within .then() vs await

A subtle conversion — when .then() returns a transformed value:

javascriptjavascript
// BEFORE
function getUpperName(id) {
  return fetchUser(id)
    .then((user) => user.name.toUpperCase()); // Returns string
}
 
// AFTER: two equivalent options
async function getUpperName(id) {
  const user = await fetchUser(id);
  return user.name.toUpperCase(); // Works — return value is auto-wrapped in Promise
}
 
// when the then handler returns a plain expression, no await needed:
async function getUpperNameAlt(id) {
  return (await fetchUser(id)).name.toUpperCase();
}

Conversion 7: Chained then with Shared Context

When a chain's later steps need access to an earlier result, Promise chains use nesting or shared variables. async/await handles this naturally:

javascriptjavascript
// BEFORE: nesting to access both user and posts
function loadUserPage(userId) {
  return fetchUser(userId).then((user) => {
    return fetchPosts(user.id).then((posts) => ({
      user,    // user is in scope via closure
      posts,
    }));
  });
}
 
// AFTER: natural variable scoping
async function loadUserPage(userId) {
  const user = await fetchUser(userId);   // user available in entire function
  const posts = await fetchPosts(user.id);
  return { user, posts };
}

Wrapping Legacy Callback APIs

Before converting to async/await, callback-based APIs must first be wrapped in Promises:

javascriptjavascript
// Legacy callback API
function readFileCb(path, callback) {
  fs.readFile(path, "utf8", callback);
}
 
// Step 1: Wrap in Promise
function readFilePromise(path) {
  return new Promise((resolve, reject) => {
    fs.readFile(path, "utf8", (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}
 
// Step 2: Use with async/await
async function processFile(path) {
  try {
    const data = await readFilePromise(path);
    return JSON.parse(data);
  } catch (err) {
    console.error("Could not read file:", err.message);
    return null;
  }
}
 
// Node.js built-in shortcut:
const { promisify } = require("util");
const readFileAsync = promisify(fs.readFile);
 
async function processFileNode(path) {
  const data = await readFileAsync(path, "utf8");
  return JSON.parse(data);
}

Side-by-Side Quick Reference

Promise Chain Patternasync/await Equivalent
return fetchData()return await fetchData() or just return fetchData()
.then(v => transform(v))const v = await …; return transform(v)
.catch(e => fallback)try { … } catch(e) { return fallback }
Mid-chain .catch(() => alt)let x; try { x = await p } catch { x = await alt }
.finally(() => cleanup())try { … } finally { cleanup() }
Promise.all([a, b])await Promise.all([a, b])
.then(([a, b]) => ...)const [a, b] = await Promise.all(...)
Rune AI

Rune AI

Key Insights

  • The conversion is mechanical for sequential chains: .then(v => ...) becomes const v = await ..., .catch(e => ...) becomes try/catch
  • Keep Promise.all for parallel operations: Converting parallel .then() chains naively to sequential awaits is a performance regression; always use Promise.all for independent operations
  • Mid-chain .catch() recovery needs a let variable: Assign to a variable declared before the try block to make it available after the catch
  • Wrap legacy callbacks in Promises first: async/await cannot work directly with callback-style APIs — use promisify or manual Promise wrappers before converting
  • return await inside try/catch matters: return await p ensures a rejection from p is caught by the current try block; return p passes the rejection to the caller
RunePowered by Rune AI

Frequently Asked Questions

Do I need to await the final return in an async function?

Not usually. `return promise` and `return await promise` behave identically in most cases because async functions wrap the return value in a fulfilled Promise. The difference matters in try/catch: `return await promise` will catch a rejection from `promise` inside the current try block; `return promise` will not (the rejection propagates to the caller). In practice, adding `return await` inside a try/catch is safer and the performance difference is negligible.

Can I have an async function with no await?

Yes, it is valid. An `async` function without `await` still returns a Promise (fulfilled with the returned value). It is occasionally useful for interface consistency — when a function must always return a Promise regardless of whether it does async work.

Should I always migrate from .then() to async/await?

Migrate when readability improves. Sequential chains with error handling benefit greatly. Complex parallel compositions or chains with multiple recovery points can sometimes be more explicit as `.then()` chains. Mix freely — they are interchangeable.

What about async/await with generators?

Before async/await was standardized, a pattern using generators with libraries like `co` achieved similar syntax. Async/await replaced that pattern. Modern JavaScript has no reason to use generator-based async patterns for typical async work.

Conclusion

Converting Promise chains to async/await is mechanical once you internalize the rules: .then() becomes await, .catch() becomes try/catch, and .finally() becomes finally. The main trap to avoid is accidentally converting parallel Promise.all operations into sequential awaits. For legacy APIs, always wrap callbacks in Promises first. Understanding the relationship between Promises and async/await makes both styles interchangeable tools in your async toolkit.