How does error propagation work in JavaScript?

JavaScript

The short answer

When an error is thrown, JavaScript stops executing the current function and travels back up the call stack looking for a try...catch block. If it finds one, the catch block handles the error. If it reaches the top of the call stack without finding a handler, the error becomes an unhandled exception and the program crashes (or the browser logs it to the console).

How errors propagate up the call stack

function c() {
throw new Error('Something broke');
}
function b() {
c(); // error thrown here, not caught
}
function a() {
b(); // error passes through here too
}
try {
a();
} catch (error) {
console.log('Caught:', error.message); // "Caught: Something broke"
}

The error was thrown in c, passed through b (no catch), passed through a (no catch), and was finally caught in the outer try...catch. Each function was removed from the call stack as the error "bubbled up."

Catching at different levels

You can catch errors at any level in the call stack:

function riskyOperation() {
throw new Error('Disk full');
}
function saveFile() {
try {
riskyOperation();
} catch (error) {
// Handle at this level
console.log('Save failed:', error.message);
// Optionally re-throw if the caller should also know
throw new Error('Could not save file');
}
}
try {
saveFile();
} catch (error) {
console.log('Operation failed:', error.message);
}

saveFile catches the error, handles it (logs it), and throws a new, higher-level error. The outer code catches that new error.

With async/await

Errors in async functions propagate through rejected promises:

async function fetchData() {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error('Fetch failed');
}
return response.json();
}
async function loadDashboard() {
const data = await fetchData(); // error propagates here
renderDashboard(data);
}
// Catch at the top level
loadDashboard().catch((error) => {
console.log('Dashboard failed:', error.message);
});

If fetchData throws, loadDashboard does not catch it, so it becomes a rejected promise. The .catch() at the end handles it.

Unhandled errors

If no try...catch catches the error:

  • In the browser — an error event fires on window, and the error appears in the console
  • In Node.js — an uncaughtException event fires, and the process may crash
// Global error handler in the browser
window.addEventListener('error', (event) => {
console.log('Unhandled error:', event.message);
});
// For unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.log('Unhandled rejection:', event.reason);
});

Best practices

  • Catch errors at the right level — not every function needs a try...catch. Catch where you can meaningfully handle the error.
  • Do not swallow errors — if you catch an error but cannot handle it, re-throw it so the caller knows something went wrong.
  • Use specific error types — catch specific errors when possible instead of catching everything.

Interview Tip

Walk through the call stack example step by step — show how the error travels up from c through b through a until it is caught. Mention that async errors propagate through rejected promises. Knowing about unhandledrejection shows you think about production error handling.

Why interviewers ask this

This question tests if you understand JavaScript's error handling model. Interviewers want to see if you know how errors move through the call stack, where to catch them, and what happens when they are not caught. It is fundamental knowledge for writing robust applications.