Refactoring Promise Chains to async/await
async/await and Promise chains are two syntaxes for the same underlying behavior. Knowing both means you can read any codebase (older code uses chains; modern code uses async/await) and choose the right style for each situation.
A systematic refactoring
Section titled “A systematic refactoring”Start with a Promise chain:
function loadApp() { showSpinner(); let savedRates;
return fetchRates() .then(rates => { savedRates = rates; return loadStoredExpenses(); }) .then(expenses => { renderExpenses(expenses, savedRates); }) .catch(error => { showError(error.message); }) .finally(() => { hideSpinner(); });}The savedRates variable exists only to pass a value from one .then to a later .then — a common chain awkwardness.
Refactored to async/await:
async function loadApp() { showSpinner(); try { const rates = await fetchRates(); const expenses = await loadStoredExpenses(); renderExpenses(expenses, rates); } catch (error) { showError(error.message); } finally { hideSpinner(); }}rates is just a local variable — no bridging needed. Both calls are in scope in the same try block.
When Promise chains are clearer
Section titled “When Promise chains are clearer”async/await is usually cleaner, but Promise chains have their place:
// Clean as a chain — each step is a named transformfetchRates() .then(filterRates) .then(formatRateDisplay) .then(renderRateTable) .catch(showError);Each .then is a named function — the chain reads like a pipeline of transformations. Converting this to async/await would require intermediate variables and lose the clear pipeline structure.
Use chains when you have a linear series of named transformation functions. Use async/await when you have conditional logic, loops, multiple variables in scope, or try/catch.
Mixing styles safely
Section titled “Mixing styles safely”An async function returns a Promise, so you can .then/.catch on it:
async function fetchRates() { /* ... */ }
// Called with .then from a non-async context:fetchRates() .then(rates => renderCurrencyPicker(rates)) .catch(err => showError(err.message));And inside an async function, you can await any function that returns a Promise — whether it uses chains internally or async/await:
async function init() { const rates = await fetchRates(); // fetchRates can use chains internally, doesn't matter}The boundary is invisible to the caller. Pick the style that reads best for each function.
Common mistakes when converting
Section titled “Common mistakes when converting”Forgetting await:
// ✗ response is a Promise, not a Responseconst response = fetch(url);
// ✓const response = await fetch(url);Using await outside async:
// ✗ SyntaxErrorfunction loadApp() { const rates = await fetchRates();}
// ✓async function loadApp() { const rates = await fetchRates();}Not catching errors on the async function call:
// ✗ Unhandled rejection if loadApp throwsloadApp();
// ✓ Handle itloadApp().catch(err => console.error(err));// orasync function safeLoad() { try { await loadApp(); } catch (e) { console.error(e); } }safeLoad();The final api.js using async/await
Section titled “The final api.js using async/await”Here is the complete api.js written in async/await style — the version BudgetBuddy uses:
const BASE_URL = 'https://open.er-api.com/v6/latest';const SUPPORTED_CURRENCIES = ['USD', 'EUR', 'GBP', 'CAD', 'JPY', 'AUD', 'CHF', 'MXN'];
let cachedRates = null;let lastFetchTime = null;const CACHE_TTL = 60 * 60 * 1000;
export async function fetchRates(base = 'USD') { const now = Date.now(); if (cachedRates && lastFetchTime && now - lastFetchTime < CACHE_TTL) { return cachedRates; }
const response = await fetch(`${BASE_URL}/${base}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json(); if (data.result !== 'success') throw new Error(`API: ${data['error-type']}`);
cachedRates = SUPPORTED_CURRENCIES.reduce((acc, code) => { if (data.rates[code] !== undefined) acc[code] = data.rates[code]; return acc; }, {}); lastFetchTime = now; return cachedRates;}
export function convertAmount(amount, from, to, rates) { if (from === to) return amount; const fromRate = rates[from]; const toRate = rates[to]; if (!fromRate || !toRate) throw new Error(`Unknown currency: ${from} or ${to}`); return (amount / fromRate) * toRate;}Exercise
Section titled “Exercise”- Take the Promise chain version of
loadAppfrom the top of this lesson and rewrite it using async/await. - Take an async/await function you wrote and rewrite it as a Promise chain. Which reads better?
- Write a function that is genuinely cleaner as a Promise chain (a linear series of named transforms). Write the same as async/await — compare.
- Fix the three common mistakes: add missing
await, add missingasync, add error handling to an unhandled call.
- async/await is syntactic sugar over Promises — both produce identical behavior.
- async/await is usually cleaner for multi-step logic, conditionals, and variables shared across steps.
- Promise chains are often cleaner for linear pipelines of named transformation functions.
- Both styles interoperate:
await asyncFn()andasyncFn().then(...)both work. - Forgetting
awaitis the most common async/await mistake — the variable holds a Promise, not the value.