What happens when JavaScript throws an error

An error interrupts the normal flow of the code currently running. JavaScript looks for a matching catch block around that work. If it does not find one, the error moves to the caller and continues upward. An error that reaches the top without being handled is reported by the environment. This is useful behavior: a failure should not quietly turn into a result that looks valid.

You can try the examples here in a modern browser console or a recent Node.js environment. They use plain JavaScript and do not depend on a page, package, or network request. To see propagation, define function readName(user) { return user.name; } and call readName(null). Accessing name on null throws a TypeError. A call to readName does not have to contain its own catch; the code that called it can decide whether it knows how to respond.

Use throw to reject invalid input

The throw statement lets your code signal a condition it cannot handle as a normal result. For example: function doublePositive(value) { if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) { throw new RangeError("Expected a positive finite number"); } return value * 2; } Calling doublePositive(4) returns 8; calling doublePositive(-1) throws. The check also rejects NaN and Infinity, which a simple value <= 0 test would miss.

JavaScript allows you to throw almost any value, but an Error object is usually the clearest choice. It carries a message and commonly provides stack information for debugging. Built-in types such as TypeError and RangeError can make the reason more specific. Write messages that identify the failed expectation without putting passwords, tokens, or other sensitive input into them.

Not every inconvenient result is an exception. A search that finds no match may reasonably return undefined or an empty array. Throw when continuing with a returned value would misrepresent a failure, and make that choice consistent for callers.

Catch errors where you can respond

A try block marks code whose thrown errors you want to handle. For example: try { const result = doublePositive(-1); console.log(result); } catch (error) { console.log("Could not calculate:", error.message); } The console.log inside try does not run after doublePositive throws. Execution instead enters catch, where error refers to the thrown value.

Keep the try block as narrow as practical. If it contains several unrelated operations, a catch may hide which one failed or handle a programming mistake as though it were expected input. A useful catch does something deliberate: asks for corrected input, supplies a safe fallback, adds context, or lets a caller know the operation failed. Avoid an empty catch that makes the problem disappear.

If a layer cannot recover, it can throw again. For instance, catch (error) { console.error("Calculation failed", error); throw error; } preserves the original error for an outer handler. Because JavaScript permits throwing non-Error values, code handling an unfamiliar library or callback should not assume error.message always exists. Your own functions can be easier to use by consistently throwing Error objects.

Put cleanup in finally

A finally block runs after try, and after catch if there is one, whether the operation succeeded or threw. This makes it a place to restore temporary state. Try this in the console: let busy = false; try { busy = true; console.log(doublePositive(2)); } catch (error) { console.log(error.message); } finally { busy = false; } The assignment in finally happens whether doublePositive returns or throws.

Finally does not automatically handle an error. If there is no catch, the error continues upward after finally runs. Even with a catch, avoid returning or throwing a new error from finally unless you intend to replace what was happening before it ran. A return in finally can override both an earlier return value and a thrown error, making failures difficult to spot.

Use finally for cleanup that must happen on both paths, not for displaying every error to a user. The code that understands the operation should decide what a failure means; finally should restore the state it owns.

Know the boundary of a try block

A try block catches errors thrown while its code is executing; it does not automatically catch work scheduled to run later. For example, try { setTimeout(() => { throw new Error("Later failure"); }, 0); } catch (error) { console.log("Caught", error); } will not log "Caught" for that error. The callback runs after the surrounding try and catch have finished. Handle a callback error inside the callback or use an appropriate error-handling boundary for the surrounding application.

Promises need similar care. In an async function, place await inside try to catch a rejection: async function loadValue() { try { return await Promise.reject(new Error("Unavailable")); } catch (error) { return "fallback"; } } Without await, returning a promise from try does not give that local catch a chance to handle its later rejection. At the call site, await the async function in another try block or attach a rejection handler if it may still fail.

The practical rule is to handle a failure at a boundary that can make a useful decision. Throw a clear Error when an operation cannot produce its promised result, catch it where recovery is possible, and use finally for cleanup that must occur either way.

Back to category ↗