The Prof's law of weird stuff applies here: focus on the constructs that look odd next to Java or C++, because they show what is different about the language. If you know exceptions from another language, most of try and catch will be familiar, and the odd parts are few and specific. Promises and await may be less familiar, and they depend on a rule about when code runs. This page covers the language only. The JavaScript engine and the event loop explains why the browser runs code in this order, and Client-Side Communication applies all of this to the network.
1. JavaScript's silent failings
Most browsers give minimal feedback that a JavaScript failure is occurring. A visitor sees a button that does nothing. The only place the error message appears is the console in the browser's developer tools, which visitors do not open. Getting script into a page introduced the console for this reason, and you should keep it open whenever you work on a page.
Silent errors were not always the default. Internet Explorer could open a dialog for each script error, with a line number, a character position, a message such as 'undefined' is not an object, and the question "Do you want to continue running scripts on this page?" Older browsers also put a small error indicator in the lower left corner of the status bar. These alerts went away because they confused end users, who could do nothing about a programmer's mistake. The cost of that decision falls on you, because a broken page now looks the same as a working one until someone tries to use it.
Try a small experiment. Open the console and browse your usual sites with it showing. Many sites log errors while appearing to work normally.
Some mistakes never become errors
JavaScript also fails more quietly than most languages, because many operations that would be errors elsewhere produce a value. Reading a missing property gives undefined. Doing arithmetic on something that is not a number gives NaN. Neither stops the program, so the bad value travels on until something finally cannot use it, often far from the mistake. Types and coercion covers these conversions.
What an uncaught exception stops
An exception that nothing catches ends the script that is currently running, and only that script. Other scripts on the page still run, and event handlers that were already attached still fire. The page ends up partly working. Whatever the failed script had not yet done, such as attaching a handler further down the file, never happens.
2. Throwing and catching
throw and the Error types
The throw statement stops the current function and hands a value to the nearest enclosing catch, walking back up through the callers until it finds one. The usual value is an Error object, which has a name, a message and a stack trace.
The language throws a few types of its own, and all of them inherit from Error. The ones you will see constantly are TypeError, for an operation on the wrong kind of value, such as calling something that is not a function or reading a property of undefined, and ReferenceError, for a name that does not exist. SyntaxError means the parser rejected some code, and JSON.parse also throws it on bad input. RangeError means a number is outside the allowed range. URIError, EvalError and AggregateError are rarer. You can throw any of them yourself.
Here is the first odd part. throw accepts any value at all, including a string or a number. A thrown string has no stack trace and no message, so code that catches it and reads error.message gets undefined. Always throw an Error or one of its subclasses. Since other people's code may not follow that rule, a catch cannot be certain of what it received.
try, catch and finally
A try block runs until something in it throws. If nothing does, the catch block is skipped. If something does, the rest of the try block is skipped and catch runs with the thrown value. The finally block runs in both cases, even when the try or the catch has already reached a return. Use it for cleanup, such as turning off a loading indicator. You can leave out either catch or finally, though not both. If catch does not use the value, you can leave out its parameter too and write catch { }.
finally runs on every path. What happens after it depends on whether an exception is still on its way up.The playground runs one function twice, once with good input and once with bad, so you can follow both paths. Each call returns a string from a different block, and finally runs before that string gets back to the caller.
If you tried that change, you saw the odd rule about finally. A return in it replaces whatever the function was about to do, including a pending exception. The error below vanishes with no trace, so keep return, break and throw out of finally.
Catching only what you can handle
Java and C++ let you write one catch per exception type. JavaScript has a single catch that receives everything, including your own typos, so a broad catch can hide a bug that has nothing to do with the failure you planned for. The standard pattern is to test the type with instanceof, handle what you expected, and throw the rest again. A small subclass of Error gives you a type to test for. Prototypes and classes covers extends.
An empty catch block returns you to the silent failure this page started with. If you truly mean to ignore an error, leave a comment that says why.
When you catch a low-level error and throw a clearer one, pass the original as cause, so that the person debugging can still see what happened. MDN lists cause as widely available since September 2021.
Two things try cannot catch
A try only covers code that runs while it is active. The first limit is parsing. The browser parses a whole script before running any of it, so a syntax error in the file stops the script before the try exists. The SyntaxError you can catch is the one thrown at run time by a function such as JSON.parse.
The second limit is time. A function handed to setTimeout runs after the try block has finished, so its exception has nothing to catch it. The call to setTimeout itself succeeded, and that call is the only thing the try covered. This limit is the reason JavaScript needed better tools for asynchronous code, as sections 5 to 7 show.
3. Reading a stack trace
A stack trace is the list of function calls that were active when an Error was created. It tells you where the failure surfaced and how the program got there. This small script fails because the cart is empty.
Chrome reports it like this, with the URLs shortened here to the file name.
Start with the message. It says the code tried to read price from undefined, so the question to answer is which value was undefined and why. The top frame shows where the read happened, in priceOf on line 2. The fault is often further down the list. priceOf did what it was told, and the frames below show who gave it a missing item: totalFor asked for items[0] of an empty array. Work down the list until you reach the line where a good value became a bad one.
The format depends on the browser. Here is the same run in Firefox, then in Safari.
Function names and line numbers agree, but the column numbers differ, and the stack text from these two browsers has no message line. The message itself varies as well. Safari says undefined is not an object (evaluating 'item.price'), close to the wording in the old Internet Explorer dialog. MDN notes that error.stack is not part of any standard, although all major engines implement it and the TC39 committee is working on standardizing it. Read it and log it, but do not write code that parses it.
A few habits make traces easier to read. Name your functions, because an anonymous function gives you a frame with no name in it. Expect production traces to be harder, since minification shortens names and puts everything on line 1. A source map lets DevTools show the original source again. Traces also get long when a framework or library is involved, and the frames you want are usually the ones in your own files.
In this playground, error.stack is a string you can print. Its locations look different from those above, because playground code runs through eval inside a sandboxed frame, and the bottom of the list adds frames from the playground's own code, but your three functions appear in the same order.
4. Debugging in DevTools
The labels in this section come from Chrome's DevTools. Other browsers have matching tools under their own names. Firefox's are in its Debugger panel, for example.
The console
console.log is the tool everyone reaches for first, and it does the job for many bugs. Its relatives help when the output gets busy: console.error and console.warn set a level you can filter on, console.table lays out an array of objects as rows, console.group indents related lines, and console.trace prints a stack trace without any error.
One behavior misleads people. When you log an object, the console keeps a reference and reads its contents when you expand the entry, so you may see values from after the log call. To record the state at that moment, log a copy.
Breakpoints
A breakpoint pauses the script at a line so that you can look around while it is stopped. That shows you every variable at once, without deciding in advance what to print. Add one in the Sources panel by clicking a line number. You can also write the debugger; statement in your code, which pauses there when DevTools is open and has no effect otherwise. Take it out before you ship.
While paused, three panes answer most questions. Scope shows the local, closure and global variables, and lets you edit their values. Call Stack is the stack trace as a live view, and selecting a frame shows you that function's variables. Watch keeps an expression of your choosing up to date as you step. The console also works in the paused context, so you can type any expression and evaluate it against the current variables.
The stepping controls move you forward. Step over runs the current line and stops on the next one. Step into goes inside a function the line calls. Step out finishes the current function and stops in its caller. Resume script execution runs until the next breakpoint.
Plain breakpoints get tedious in a loop, so learn these variations as well:
- A conditional breakpoint pauses only when an expression is true, such as
items.length === 0. Right-click the line number to add one. - A logpoint writes a message to the console without pausing, and needs no edit to your source. It is added from the same menu.
- Pause on uncaught exceptions stops at the failing line with all its variables available, before the stack unwinds.
- Pause on caught exceptions stops even when a
catchwill handle the error, which finds failures that some code is hiding. Firefox has the same pair of options in its Breakpoints list. - Event listener breakpoints pause when a chosen event fires, which helps when you do not know what code runs on a click.
Open a page of your own, set a breakpoint in a function, and trigger it. Then step into a call, watch a value change in the Scope pane, and select a lower frame in the Call Stack.
5. Callbacks
A page has one thread running its script, and that same thread handles rendering and input. So a function that needs to wait for a timer, a click or a server cannot sit in a loop and wait, because the page would freeze. The event loop module explains this model. At the language level, it means asynchronous work has two parts: you start something now, and you say what should happen when it finishes.
The oldest way to say that is to pass a function. You have already seen one, because functions are values in JavaScript, as Functions and closures shows. setTimeout takes a function and calls it later, and the code after it keeps going in the meantime.
Since a try cannot catch a callback's failure, callback code has to pass errors along by hand. A common convention, used heavily in Node, makes the first argument of the callback an error, or null when there is none. The functions below stand in for any slow work. They use timers so that you can run them, and a request to a server has the same shape.
One step in this style is readable. Trouble starts when each step depends on the one before it, because every step has to nest inside the last and handle its own error. The result is known as callback hell, or the pyramid of doom.
The nesting is the visible problem. There are others. Forget one error check and that failure disappears. If a callback throws, nothing above it can catch the exception. You also give another function control over your code, and you must trust it to call your function exactly once. Callbacks are still the correct tool for an event that happens many times, such as a click, which is why event handlers use them. They fit poorly with a single result that arrives later.
6. Promises
The 2015 edition of the language added promises to address asynchronous-style coding. A promise is an object that stands for a result you do not have yet. A function that used to take a callback now returns a promise right away, and you attach your handlers to that object.
resolve or reject are ignored.You will mostly consume promises that other functions return, but creating one shows how they work. The function you pass to new Promise runs immediately, and it receives two functions for settling the promise. Handlers attached with .then always run later, even when the promise has already settled. That guarantee removes a whole category of bug, because a handler can never run before the code that attached it has finished.
Chains flatten the pyramid
Every call to .then returns a new promise, and that new promise settles according to what the handler does. Three rules cover it. If the handler returns a value, the next .then receives that value. If it returns a promise, the chain waits for it and passes along its result. If it throws, the new promise is rejected, and the chain skips forward to the next .catch. Here is the pyramid again, with each step returning a promise.
The one .catch handles a failure from any step, including an exception thrown by accident in a handler. A .catch also recovers: if it returns normally, the chain continues with .then as though nothing failed. .finally runs on either path and passes the result through, like the finally block it is named after. The playground shows a value moving down a chain until a throw diverts it.
The most common mistake with chains is a forgotten return. If a handler starts a promise and does not return it, the chain has no way to wait for it, and the next handler runs early with undefined. MDN's guide to using promises calls this a floating promise. Its failure is also out of reach of any .catch on the chain.
Waiting on several at once
Promise has four static methods that take a collection of promises and return one. They differ in what they wait for and in what counts as failure.
| Method | Fulfills | Rejects |
|---|---|---|
Promise.all | When all fulfill, with an array of values in input order | On the first rejection |
Promise.allSettled | When all settle, with an array of status objects | It does not reject because of its inputs |
Promise.race | If the first to settle fulfills | If the first to settle rejects |
Promise.any | On the first fulfillment | When all reject, with an AggregateError |
Promise.all is the one you will use most, and two details are easy to miss. Its results keep the order you passed in, regardless of which finished first. And a rejection does not cancel the other operations: they keep running, and their results are ignored. When you want every outcome, including the failures, use Promise.allSettled.
7. async and await
The 2017 edition added async functions, which use promises with syntax that reads like ordinary sequential code. There are only two keywords. Putting async before a function makes it always return a promise. A returned value fulfills that promise, and a thrown exception rejects it. Inside such a function, await pauses the function until a promise settles, then either produces the value or throws the rejection reason at that line.
That last rule is what brings try and catch back to asynchronous code. Compare this with the chain in the last section.
These are still promises. loadUser returns one, and so does showTotal. user and orders are also both in scope at the end, which took extra work in a chain, where each handler has its own variables.
await pauses the function, not the program
The sequential appearance leads people to assume await blocks like a sleep call in another language. It does not. An async function runs synchronously up to its first await, and at that point it returns its promise to the caller, whose code carries on. The function picks up where it left off later. Other code runs in the gap, which includes other calls to the same function.
This has a consequence for your state. In synchronous code, nothing else can change your variables between two lines. Across an await, something can. A value you read before the await may be stale after it, so check it again if it could have changed.
await is allowed inside async functions and at the top level of a module. In a classic script, including this site's playgrounds, a top-level await is a SyntaxError, so the samples here wrap it in a function. Programming in the large covers modules.
In sequence or at the same time
Each await holds the function until that one result is in, so two in a row run one after the other, even when the second does not depend on the first. To run independent work at the same time, start it all first and then wait on the group with Promise.all. Here the slower task is listed first and finishes last, and the results still come back in the order they were requested.
Loops have a trap of their own. forEach ignores what its callback returns, so an async callback's promise is dropped, and forEach returns before any of the work is done. A for/of loop inside an async function does wait at each await.
Reading it later
Promises came with a batch of newer syntax, along with its various little quirks and wrinkles, and you are encouraged to explore it. Be careful as you do. Readability and writeability are in tension in programming, so don't get too fancy with your syntax without considering the readability price for you or others later on. A long .then chain of one-line arrow functions is quick to write and slow to read, and so is an await nested inside a call inside a template string. You will read both .then and await in other people's code, since they are the same machinery, and you should be able to translate one into the other.
Everything here applies directly to the network, since fetch returns a promise. It also has failure rules of its own, including that a 404 response does not reject. Patterns: fetch, and what changed covers that, and When it goes wrong covers timeouts and retries.
8. Predicting the output order
You should be able to read asynchronous code and say what order it runs in. Three rules cover nearly everything you will meet.
- The current script or callback runs to its end, without interruption. Nothing asynchronous can cut in.
- Then every microtask runs. Promise handlers, the code after an
await, andqueueMicrotaskcallbacks are microtasks. Any new microtask added along the way also runs in this round. - Then the next task runs, such as a timer callback or an event handler, and the cycle returns to rule 2 before the task after it.
These are the language-side results of the event loop. Module 6 has the full model of tasks and microtasks, and where rendering fits among them.
Predict the order for the playground below, and write your answer down before you read its last lines.
then comes before async after await because of when each was queued. The promise was already fulfilled, so .then queued its handler at that line. run() reached its await later and queued its continuation behind it. If you moved the run() call up, as the comment suggests, those two lines swapped.
The next sample shows the last part of rule 2 and the last part of rule 3. A microtask added by a microtask still runs before any timer, but it goes behind those already in the queue. A microtask added by a timer runs right after that timer and before the next timer.
Rule 1 also means a timer's delay is a minimum. It says the callback may not run before that much time has passed, and it still has to wait for the current code to finish.
Use these rules to read code, and avoid writing code that depends on the fine order of microtasks from unrelated promises. That order is hard for the next reader to see, and it has changed before: the V8 team's write-up describes how an await on a native promise went from a minimum of three microtask ticks to one, with the specification updated to match. If one thing has to happen after another, make that explicit by chaining or awaiting it.
9. Rejected promises
The unhandled rejection
A rejected promise that has no handler is the asynchronous form of an uncaught exception. Nothing stops, the rest of your script carries on, and the only sign is a console message. The browser also fires an unhandledrejection event, and the message appears unless a listener cancels that event. Because an async function reports every exception by rejecting its promise, calling one without handling the result means any error in it ends up here.
In this playground, save throws and nothing is listening for its promise. The console shows the script keeps going, then the error, in that order. Fix it by following the comment. Then the error is handled and the console entry changes from an error to a plain line.
A handler can arrive a little late without penalty, as long as it arrives before the microtasks finish. The browser reports a rejection only once that round is over. If the handler comes in a later task, you get the report first, and then a rejectionhandled event when the handler is attached. Attach handlers when you create the promise, and this will not come up.
Other hosts treat this more severely. Since version 15, Node's default raises an unhandled rejection as an uncaught exception, which ends the process unless something handles it.
Where the catch has to be
The most common version of this bug is a try that looks right. Without await, the call only starts the work and returns a promise, which succeeds. The try block is finished long before the rejection.
The same rule explains a subtler case. return save() inside a try hands the promise straight to the caller, so that function's catch never sees a rejection. return await save() waits inside the try, and the catch gets its chance.
Two more cases follow the same logic. The second argument to .then handles a rejection from earlier in the chain, and it does not cover the handler beside it. A .catch further down does.
A promise turns exceptions into rejections only while its own code is running. A throw inside the executor rejects the promise. A throw inside a timer callback started by that executor happens after the executor has returned, so it is an ordinary uncaught exception, and the promise stays pending forever. This is the limit from section 2 again, and it is why callback-based code should be wrapped so that it calls reject.
The last resort
A page can listen for both kinds of failure that nothing handled. error fires on window for an uncaught synchronous exception, and unhandledrejection fires for a rejection. Neither one rescues anything: the script that threw has still stopped. Their purpose is to report the problem, since visitors see no error and your own console is not on their machine. Calling event.preventDefault() in the rejection handler stops the browser's default console message, so use it only if you are recording the failure elsewhere.
These listeners are a safety net. Handle a failure where you can do something about it, usually where you can tell the visitor what happened, such as a message that the save failed and a way to try again. A catch that only logs still leaves the visitor with the silent failure from the start of this page.
10. Summary
| Concept | Key points |
|---|---|
| Silent failure | Visitors see nothing, so keep the console open. Many mistakes produce undefined or NaN and no exception. An uncaught exception ends only the current script. |
| throw | Accepts any value. Throw an Error or a subclass, which carries a message and a stack trace. |
| try, catch, finally | finally runs on every path, and a return in it overrides everything. There is one catch per try, so test with instanceof and rethrow the rest. Keep the original with cause. |
| Limits of try | It cannot catch a syntax error in its own script, or a failure in code that runs after it has finished. |
| Stack traces | Read the message, then the frames from the top down, until you reach where the value went bad. The format varies by browser and is not standard, so do not parse it. |
| DevTools | Breakpoints, conditional breakpoints and logpoints; the Scope, Call Stack and Watch panes; pause on uncaught or caught exceptions; debugger;. A logged object shows its contents when expanded. |
| Callbacks | Pass a function to run later. They fit repeating events. Nesting and hand-passed errors make them poor for sequences of single results. |
| Promises | Pending, then settled once as fulfilled or rejected. Handlers always run later. .then returns a new promise, so return from handlers. .catch recovers. Promise.all keeps input order and cancels nothing. |
| async and await | An async function always returns a promise. await pauses the function, not the program, and throws a rejection at that line. forEach does not wait; for/of does. |
| Output order | The current code runs to its end, then every microtask, then the next task. A timer's delay is a minimum. Do not depend on the fine order of unrelated microtasks. |
| Rejected promises | An unhandled rejection does not stop the script, and in Node it ends the process by default. try needs await to catch one. unhandledrejection and error on window are for reporting. |
