Skip to content

Error Handling with try/catch

In Promise chains, errors propagate through .catch. In async/await code, the equivalent is try/catch — the same syntax you would use for synchronous code.

async function fetchRates() {
try {
const response = await fetch('https://open.er-api.com/v6/latest/USD');
if (!response.ok) throw new Error(`HTTP error ${response.status}`);
const data = await response.json();
if (data.result !== 'success') throw new Error(`API error: ${data['error-type']}`);
return data.rates;
} catch (error) {
console.error('Failed to fetch rates:', error.message);
return { USD: 1 }; // graceful fallback
}
}

If any await rejects, or if any throw fires, execution jumps to the catch block — just like synchronous error handling.

error in the catch block is the rejection reason — whatever was thrown or rejected with. It is usually an Error object:

try {
const data = await fetchRates();
} catch (error) {
console.error(error.message); // human-readable message
console.error(error.stack); // full stack trace for debugging
}

finally works the same as in Promise chains — runs after the try/catch regardless of outcome:

async function loadApp() {
showSpinner();
try {
const rates = await fetchRates();
renderExpenses(expenses, rates);
} catch (error) {
showError('Could not load exchange rates.');
} finally {
hideSpinner(); // runs whether it succeeded or failed
}
}

Use multiple try/catch blocks when different errors need different responses:

async function initBudgetBuddy() {
let rates;
try {
rates = await fetchRates();
} catch {
// exchange rates optional — degrade gracefully
rates = { USD: 1 };
}
try {
const storedExpenses = loadExpenses();
renderExpenses(storedExpenses, rates);
} catch (error) {
console.error('Failed to load expenses:', error);
renderExpenses([], rates); // render empty state
}
}

Each try/catch handles one concern. A failure in one does not crash the other.

If an async function throws and the caller does not handle it, you get an unhandled Promise rejection — a warning in development and sometimes a crash in production.

Always either await inside a try/catch, or chain .catch() on the returned Promise at the call site:

// Option 1: try/catch inside
async function safeInit() {
try {
await initBudgetBuddy();
} catch (error) {
console.error('Init failed:', error);
}
}
safeInit();
// Option 2: .catch on the call site
initBudgetBuddy().catch(error => console.error('Init failed:', error));

Both are valid. Option 2 is more concise when you just need to log the error without additional recovery.

Sometimes you want to catch an error, do something specific (log it, transform it), then let it propagate:

async function fetchRates() {
try {
const response = await fetch('https://open.er-api.com/v6/latest/USD');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
return data.rates;
} catch (error) {
// Add context before re-throwing
throw new Error(`fetchRates failed: ${error.message}`);
}
}

The caller receives a more informative error without knowing the internal details of how fetching works.

  1. Rewrite the fetchRates() function from Module 05 using async/await and try/catch instead of a Promise chain.
  2. Add a finally block that logs 'fetchRates complete' regardless of outcome.
  3. Write an initBudgetBuddy() function with two separate try/catch blocks — one for fetching rates, one for loading expenses — so a failure in one does not affect the other.
  4. Deliberately call fetchRates() without await or .catch — confirm the unhandled rejection warning appears in the console.
  • try/catch in async functions catches both thrown errors and rejected awaited Promises.
  • catch (error) receives the rejection reason — an Error object with message and stack.
  • finally runs after the try/catch regardless of outcome.
  • Use multiple try/catch blocks when different errors need different recovery strategies.
  • Always handle errors from async functions — either with try/catch or by chaining .catch at the call site.