Skip to content

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.

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.

async/await is usually cleaner, but Promise chains have their place:

// Clean as a chain — each step is a named transform
fetchRates()
.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.

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.

Forgetting await:

// ✗ response is a Promise, not a Response
const response = fetch(url);
// ✓
const response = await fetch(url);

Using await outside async:

// ✗ SyntaxError
function loadApp() {
const rates = await fetchRates();
}
// ✓
async function loadApp() {
const rates = await fetchRates();
}

Not catching errors on the async function call:

// ✗ Unhandled rejection if loadApp throws
loadApp();
// ✓ Handle it
loadApp().catch(err => console.error(err));
// or
async function safeLoad() { try { await loadApp(); } catch (e) { console.error(e); } }
safeLoad();

Here is the complete api.js written in async/await style — the version BudgetBuddy uses:

api.js
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;
}
  1. Take the Promise chain version of loadApp from the top of this lesson and rewrite it using async/await.
  2. Take an async/await function you wrote and rewrite it as a Promise chain. Which reads better?
  3. Write a function that is genuinely cleaner as a Promise chain (a linear series of named transforms). Write the same as async/await — compare.
  4. Fix the three common mistakes: add missing await, add missing async, 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() and asyncFn().then(...) both work.
  • Forgetting await is the most common async/await mistake — the variable holds a Promise, not the value.