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.
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 => ...)becomesconst value = await ....catch(err => ...)becomestry { ... } catch (err) { ... }- Returning a value in
.then()becomes returning a value directly - Returning a Promise in
.then()becomes awaiting that Promise
// 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
// 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()
// 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:
// 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 readableConversion 4: .finally()
// 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:
// 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:
// 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:
// 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:
// 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 Pattern | async/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
Key Insights
- The conversion is mechanical for sequential chains:
.then(v => ...)becomesconst v = await ...,.catch(e => ...)becomestry/catch - Keep Promise.all for parallel operations: Converting parallel
.then()chains naively to sequential awaits is a performance regression; always usePromise.allfor 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
promisifyor manual Promise wrappers before converting - return await inside try/catch matters:
return await pensures a rejection frompis caught by the current try block;return ppasses the rejection to the caller
Frequently Asked Questions
Do I need to await the final return in an async function?
Can I have an async function with no await?
Should I always migrate from .then() to async/await?
What about async/await with generators?
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.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.