completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Exceptions & unwinding

Learn how JavaScript engines find exception handlers, unwind stack frames, run finally blocks, and carry errors across async boundaries.

By the end, you can
  • 01
    Follow a thrown errorExplain how V8 uses a handler table to find the right catch, runs each finally on the way, and takes calls off the stack until a catch handles the error.
  • 02
    Predict what finally doesTell when a finally block keeps a return, replaces a throw, runs on break or continue, and leaves a value alone.
  • 03
    Know what really costs timeTell apart having try/catch in your code, which is cheap today, from throwing, recording the stack trace, and errors that happen later.

What happens after a throw

You already know try/catch. You put code inside try. If that code throws an error, the catch part runs. Earlier lessons showed how to make and handle errors: custom errors, error strategies, promise errors, and reading errors.

This lesson shows what the JavaScript engine does after a throw. The engine is the program that runs your code. Chrome and Node use an engine called V8.

A stack frame is the record of one function call that is still running. The stack frames lesson shows what a frame holds. Frames sit on the call stack. Each new call goes on top. It comes off again when the function ends.

A throw stops the current line at once. The engine then has to find code that can deal with the error. On the way, it must still run every finally block it passes.

Definition

An exception is a thrown error. Exception unwinding is what the engine does after a throw:

  • It looks in the current call for a catch or finally around the line that threw.
  • It runs any finally code it must run.
  • It takes calls with no catch off the stack.
  • It keeps going outward until a catch handles the error. If none does, the browser or Node reports an "uncaught" error.
Real-life analogyA fire alarm leads people to an exit

When a fire alarm rings, people go down floor by floor until they reach an exit. The exit signs were put up long before the alarm. Errors work the same way. The engine prepares its directions ahead of time. It only uses them when something throws.

In real life: Exit signs are put up before any alarm
In JavaScript: The engine writes down, ahead of time, where each try block leads
In real life: People leave one floor, then the next
In JavaScript: The error leaves one call, then the next, until a catch handles it
In real life: Each floor is closed up before people leave it
In JavaScript: finally runs its cleanup as the error leaves a call
In real life: No exit is found, so someone must help
In JavaScript: No catch is found, so the browser or Node reports an uncaught error

This lesson comes after Suspending frames: generators & async functions, because await changes where an error comes back. Next comes Why garbage collection?, which moves from the call stack to memory.

Handler tables: where to go when code throws

V8 BYTECODE

Before V8 runs a function, it turns it into bytecode. Bytecode is a list of small, simple steps that the engine runs one by one. Each step has a number, called its offset.

A handler table is a list the engine keeps next to that bytecode. Each row says: "if a step in this range throws, jump to this step." The step it jumps to is the handler: the start of the catch or finally code.

When nothing goes wrong, the engine never reads the table. It reads it only when code leaves a try block early. A throw does that. So does a return that has to pass through a finally first.

Node only: a function with try, catch, and finallyJavaScript
function handlerTableProbe(input) {  try {    if (input < 0) throw new Error("negative");    return input + 1;  } catch (error) {    return 0;  } finally {    input += 1;  }} handlerTableProbe(1);

The lesson test runs this function in Node with two options. --print-bytecode prints the bytecode. --print-bytecode-filter=handlerTableProbe limits it to this one function. The function doesn't call console.log, so its own output can't mix with V8's list.

The test checks only the parts that stay the same from run to run:

  • The Node version starts with v22.
  • The V8 version starts with 12.4.
  • The list contains the Handler Table heading, line 4 below.
The part of V8's bytecode list that the test checksText
$ node --print-bytecode --print-bytecode-filter=handlerTableProbe handler-table-probe.mjs[generated bytecode for function: handlerTableProbe]...Handler Table (size = N)# Each row: if a step in this range throws, jump to this step. Step numbers change between V8 versions.
What the handler table tells the engine. Three cards: try, lines 3 to 4. On a throw, an arrow goes to catch, line 6. After that, an arrow goes to finally, line 8.What the handler table tells the enginetry: lines 3-4on a throwcatch: line 6after thatfinally: line 8guarded codeerror handlercleanup
Lines 3–4 are inside try. If they throw, the table sends the engine to the catch code on line 6, and after that to the cleanup on line 8.

Each row of a handler table has three parts. The table below names them. Read each row as: the part, what it means, and why it matters.

What each part of a handler table means
PartWhat it meansWhy it matters
Guarded rangeA range of bytecode steps inside a try, catch, or finally.The engine uses a row only if the step that threw is inside its range.
Handler offsetThe number of the step to jump to when a step in the range throws.For a catch, it is the start of the catch code. For a finally, it is the start of the cleanup, which runs before the error moves on.
Prediction noteA short note V8 keeps for itself, such as "this goes to a catch" or "this goes to a finally".It is V8's own bookkeeping. Your JavaScript code can't see it.

The exact step numbers are V8 details. Other engines store the same information in their own way. JavaScript doesn't promise a V8-style table. It only promises that catch and finally behave as the language rules say.

Unwinding: leave calls until one can catch the error

STEP THROUGH

Here is a checkout with three calls, one inside the next:

  • placeOrder starts the order. It has a catch.
  • chargeCard takes the payment. It has a finally, but no catch.
  • validateUpi checks the UPI ID, a payment address used in India, such as asha@bank. It has a finally, but no catch.

The UPI ID "asha-upi" has no @, so validateUpi throws. Step through the code to watch the error move.

Step through: an error leaves validateUpi and is caught in placeOrder
Step 0 of 13Ready
Your turn: follow the blue line

Follow an error from validateUpi to the catch in placeOrder. Watch each finally run and each call leave the stack.

Running in
  1. script
Next: line 1
function enter(label) { timeline.push("enter " + label); }function leave(label) { timeline.push(label); } function placeOrder(order) {  enter("placeOrder");  try {    const receipt = chargeCard(order);    return "confirmed: " + receipt;  } catch (error) {    return "handled in placeOrder: " + error.message;  } finally {    leave("placeOrder finally: unlock cart");  }} function chargeCard(order) {  enter("chargeCard");  try {    const authorization = validateUpi(order.upiId);    return "charged " + authorization;  } finally {    leave("chargeCard finally: release terminal");  }} function validateUpi(upiId) {  enter("validateUpi");  try {    if (!upiId.includes("@")) {      throw new Error("Invalid UPI ID");    }    return "UPI authorized";  } finally {    leave("validateUpi finally: close UPI session");  }} console.log(placeOrder({ upiId: "asha-upi" }));console.log(timeline.join(" -> "));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Watch the "top frame" label as you step. Line 31 throws inside validateUpi. Line 35 runs that function's finally, and then its call leaves the stack. chargeCard has no catch, so line 23 runs its finally, and that call leaves too. Line 11 is the first catch the error meets. There, placeOrder turns the error into a normal string.

Three calls stacked, newest on top: validateUpi, chargeCard, placeOrder. An arrow labeled the error goes down past them. validateUpi throws, runs finally, and leaves. chargeCard has no catch, runs finally, and leaves. placeOrder's catch handles it and runs finally.the errorvalidateUpichargeCardplaceOrder1. throws, runs finally, leaves2. no catch: runs finally, leaves3. catch handles it, runs finally
The error starts in the newest call, validateUpi, and moves down. Each call without a catch runs its finally and leaves. placeOrder has a catch, so the error stops there.
What unwinding does not do
  • It doesn't start any function again from the top.
  • It doesn't run the rest of the code in the calls it leaves.
  • It runs only the finally and catch code that the language rules ask for.

How finally changes the way a block ends

CONTROL FLOW

The spec is the official rule book for JavaScript. It uses the word completion for how a statement ends. There are five kinds:

  • normal: it ran to the end, sometimes with a value.
  • return: it reached a return.
  • throw: it threw an error.
  • break: it jumped out of a loop.
  • continue: it jumped to the next turn of a loop.

A finally block always runs when the code leaves its try or catch. If finally ends normally, the earlier completion goes on as before. If finally returns or throws, its new completion replaces the earlier one.

Step through: what finally replaces, and what it keeps
Step 0 of 8Ready
Your turn: follow the blue line

See how finally deals with a return, a throw, and a break, and when it keeps the try value. Every step replays what real JavaScript does.

Running in
  1. script
Next: line 27
function finallyReturnWins() {  try {    return "try return";  } finally {    return "finally return";  }} function finallyCanSwallowThrow() {  try {    throw new Error("try exploded");  } finally {    return "finally recovered";  }} const breakSteps = [];outer: for (const item of ["first"]) {  try {    breakSteps.push("try " + item);    break outer;  } finally {    breakSteps.push("finally " + item);  }} console.log(finallyCanSwallowThrow());console.log(breakSteps.join(" -> "));console.log(eval("try { 1 } finally { 2 }"));
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
finally replaces a return and a throw, runs on break, and keeps a valuePop out in the code editor (opens in a new tab)JavaScript
function finallyReturnWins() {  try {    return "try return";  } finally {    return "finally return";  }} function finallyCanSwallowThrow() {  try {    throw new Error("try exploded");  } finally {    return "finally recovered";  }} const breakSteps = [];outer: for (const item of ["first"]) {  try {    breakSteps.push("try " + item);    break outer;  } finally {    breakSteps.push("finally " + item);  }} console.log(finallyReturnWins());console.log(finallyCanSwallowThrow());console.log(breakSteps.join(" -> "));console.log(eval("try { 1 } finally { 2 }"));
finallyReturnWins. The try card says try return and is dropped. The finally card says finally return. An arrow from it shows the caller gets finally return.try wants to return"try return"droppedfinally returns"finally return"the caller gets"finally return"
In finallyReturnWins(), try wants to return "try return". Then finally returns too, and its value is the one the caller gets.

The last line is a surprise. eval runs a string as code and gives back the value of the last statement it ran. eval("try { 1 } finally { 2 }") gives 1, not 2.

The finally block did work out 2. But it ended normally, so it doesn't replace anything. The spec's rule is: when finally ends normally, keep the result of the try block. That result is 1.

Playground: choose what each block does
The function the playground runsJavaScript
function scenario(tryAction, catchAction, finallyAction) {  try {    if (tryAction === "throw") throw new Error("try threw");    if (tryAction === "return") return "try returned";  } catch (error) {    if (catchAction === "throw") throw new Error("catch threw");    if (catchAction === "return") return "catch returned";  } finally {    if (finallyAction === "throw") throw new Error("finally threw");    if (finallyAction === "return") return "finally returned";  }  return "normal completion";}
How it endedtry returned
step 1try: return
step 2finally: nothing
Step 2 of 4try returned
try block
catch block
finally block

The function ended with "try returned". Now change what finally does. If finally returns or throws, it replaces whatever try or catch did.

The playground picks one of a few fixed versions of the function below and runs it. It never runs text you type. So the result is what real JavaScript does.

Use the playground to try each mix. Pick nothing, return, or throw for each block. You'll see three rules:

  • If try returns and finally does nothing, the try return wins.
  • If finally returns or throws, finally wins.
  • If try throws and catch returns, the catch return wins, unless finally replaces it.

Does try/catch make code slow today?

MEASURE FIRST

V8 makes busy functions faster by turning them into fast machine code. This is called optimizing.

Old advice said: "never put busy code inside try/catch, because the engine can't optimize it." That was true for some older versions of V8. V8's old optimizer, Crankshaft, handled try/catch badly. In 2017, V8 replaced it with two new parts: Ignition, which runs bytecode, and TurboFan, which optimizes. Today's V8 can optimize functions that contain try, catch, and finally.

Node only: a function with try/catch/finally still gets optimizedJavaScript
// Tested in Node 22 (V8 12.4). Run: node --allow-natives-syntax file.mjsfunction protectedAdd(x) {  try {    return x + 1;  } catch (error) {    return -1;  } finally {    x = 0;  }} %PrepareFunctionForOptimization(protectedAdd);protectedAdd(1);protectedAdd(2);%OptimizeFunctionOnNextCall(protectedAdd);console.log("result", protectedAdd(3));const status = %GetOptimizationStatus(protectedAdd);console.log("optimized", Boolean(status & (1 << 4)));console.log("turbo", Boolean(status & (1 << 6)));

The commands that start with % are V8 test tools, not normal JavaScript. They work only when Node starts with --allow-natives-syntax:

  • %PrepareFunctionForOptimization (line 12) tells V8 to start collecting facts about the function.
  • %OptimizeFunctionOnNextCall (line 15) asks V8 to optimize it on the next call.
  • %GetOptimizationStatus (line 17) gives back a number. Each bit of that number answers one yes-or-no question, such as "is it optimized?"

The lesson test checks three printed lines in Node's V8 12.4: the result is 4, the optimized bit is true, and the TurboFan bit is true. So having try/catch/finally in the function did not stop V8 from optimizing it.

When you create an Error, V8 also records the list of calls that led to it. That list is the stack trace. It is saved as a string in error.stack. Error.stackTraceLimit sets how many calls V8 records.

Node and V8: turn off the stack traceJavaScript
const normal = new Error("payment failed");console.log(normal.stack.includes("\n    at")); Error.stackTraceLimit = 0;const withoutFrames = new Error("payment failed");console.log(withoutFrames.stack);console.log(withoutFrames.stack.includes("\n    at"));

Line 2 prints true, because a normal error's stack has at ... lines. Line 4 sets the limit to 0. Line 6 prints only the first line, Error: payment failed, and line 7 prints false.

The table below shows where the cost really is. Read each row as: when your code has this, this is true today, so do this.

Where try/catch really costs time
When your code hasWhat is true todayWhat to do
A try block that never throwsToday's V8 can optimize functions with try, catch, and finally.Don't remove clear error handling just to make code faster.
A thrown ErrorJavaScript makes a new object and usually records the stack trace.Throw only when something truly went wrong, not to control a normal loop.
Error.stackTraceLimit = 0V8 and Node still make an Error, but its stack string has no at ... lines.Good for a small test. It is no replacement for measuring your real app.
A catch that throws againIt runs, and then the new error moves on to the calls further out.Add details only when they help, and keep the first error as the cause.

This lesson gives no timing numbers. Timings change from machine to machine and from program to program. The safe rule is smaller: throwing is the rare path, and recording the stack trace is part of its cost. Look into it only if a profile, a recording of where your program spends its time, points there. For more on the stack string, see Stack traces & the stack trace API.

Errors that happen later

PROMISES

A normal try/catch covers only the call stack that is running right now. A timer callback or a promise handler runs later, after that stack is gone. So the old try can't catch their errors.

That is why the promise and async lessons use .catch, await, and host events. A host is the place where JavaScript runs: the browser or Node. Wrapping a try around the code that schedules the work is not enough.

Real-life analogyA phone reminder that pops up later

You set a reminder, then go on with your day. When it pops up later, the thing you were doing when you set it is over. You can only deal with the reminder now. Async code works the same way.

In real life: You read a message while using your phone
In JavaScript: A throw happens on the stack that is running now
In real life: A reminder pops up later
In JavaScript: A timer or promise callback runs later, on a new stack
In real life: You deal with the reminder when it pops up
In JavaScript: .catch or unhandledrejection deals with the later error
In real life: Opening the reminder takes you back to its task
In JavaScript: await brings the async function back and throws the error there
await throws a rejection inside the async functionPop out in the code editor (opens in a new tab)JavaScript
async function submitUpiPayment() {  try {    await Promise.reject(new Error("UPI gateway timeout"));    console.log("paid");  } catch (error) {    console.log("caught after await:", error.message);  }} submitUpiPayment();console.log("after submitUpiPayment call");

Line 10 calls the function. It pauses at the await on line 3, so line 11 prints first. Then the promise rejects. The function comes back at line 3 and throws the error there, so the catch on line 5 runs.

A promise handler runs after the try has finishedPop out in the code editor (opens in a new tab)JavaScript
try {  Promise.reject(new Error("receipt failed")).catch((error) => {    console.log("promise handler:", error.message);  });  console.log("try block finished");} catch (error) {  console.log("caught synchronously:", error.message);}

Line 2 makes a rejected promise and gives it its own .catch handler. Line 5 prints first. The handler runs later, after the try is done. The outer catch never runs.

A timer callback needs its own try/catchPop out in the code editor (opens in a new tab)JavaScript
try {  setTimeout(() => {    try {      throw new Error("timer failed");    } catch (error) {      console.log("callback handled:", error.message);    }  }, 0);  console.log("try finished before timer");} catch (error) {  console.log("outer catch:", error.message);}

Line 2 only schedules the callback. Line 9 prints, and the outer try ends. Later, the callback runs and throws on line 4. The try/catch inside the callback handles it.

Left: try around setTimeout. The try block ends, the callback runs later and throws, and the outer catch is gone. Right: try around await. The try pauses, the error comes back to it, and the catch runs, so the error is caught.try { setTimeout(...) }try block endsruns latercallback throwsouter catch is gonetry { await ... }try pausescomes backcatch runscaught
Left: the try block ends before the timer callback runs, so its catch can't help. Right: await pauses inside the try, and the error comes back there, so the catch runs.
Node only: an event for a rejection nobody handledJavaScript
process.on("unhandledRejection", (reason) => {  console.log("unhandledRejection:", reason.message);}); try {  Promise.reject(new Error("unawaited promise"));  console.log("try block ended");} catch (error) {  console.log("caught:", error.message);} setTimeout(() => {}, 0);

Line 1 listens for Node's unhandledRejection event. Line 6 makes a rejected promise that nothing handles. The try on line 5 doesn't see it, so Node fires the event instead. Line 12 keeps Node running long enough for that. Browsers fire a similar event, unhandledrejection, on window.

The table below compares four cases. Read each row as: when this happens, can the try/catch around it catch the error, and what should you do?

Which errors can a try/catch around the code catch?
What happensDoes this try/catch catch it?What to do
A throw inside try, right awayYes. The nearest catch catches it while the stack is still there.Nothing extra. The engine leaves calls until it reaches that catch.
A throw in a setTimeout callbackNo. The callback runs later, on a new stack, after the try has ended.Put a try/catch inside the callback, or send the error to one shared handler.
A rejected promise you don't awaitNo. It is not a throw. The error stays in the promise and may fire unhandledrejection.Add .catch, or return or await the promise.
await inside tryYes. When the promise rejects, the await line throws inside the paused async function.This builds on paused functions from the previous lesson.
Caught by this try/catch, or not?
  • `try { validateUpi(); } catch (e) { ... }`, and `validateUpi` throws before returning.
  • `try { setTimeout(() => { throw error; }); } catch (e) { ... }`
  • `try { await Promise.reject(error); } catch (e) { ... }` inside an async function.
  • `try { Promise.reject(error); } catch (e) { ... }` with no await and no .catch.
  • `try { throw error; } finally { return "ok"; }`
  • `catch (e) { throw new Error("with context"); }`
Try it yourself
0 of 6 correct

Sort each piece of code. Can the try/catch around it catch the error? Or does a return in finally replace the error before any catch sees it?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Using this in real code

These details help you make everyday choices in code reviews. They are no reason to build your own error system, or to avoid clear error handling.

  • Throw only when something truly went wrong: bad data, a broken rule, or a job this part of the code can't finish.
  • Catch where you can do something useful: add details, try again, show a helpful message, or turn the error into a normal result.
  • Use finally for cleanup that must run whether the code returns, throws, breaks, or continues.
  • To handle a promise's error with try/catch, await the promise inside the try. Otherwise, add .catch, or return the promise to the caller.
  • Measure before you change clear code for speed. If throwing shows up in a profile, throw less often on that busy path.

A real payment page might catch errors in the code that runs when you press "Pay". There it can log a custom PaymentError, show a helpful message, and use finally to hide the loading spinner. It should not use throws as the normal way to check whether every item in the cart is in stock.

Common misconceptions

  • "Any try/catch stops optimization." Modern V8 can optimize code with try/catch/finally. Throwing is the slow path to measure.
  • "finally always replaces the result." It always runs. But it replaces the earlier result only if it returns or throws.
  • "A try around setTimeout catches errors in the callback." The callback runs later, on a new stack, after the try has ended.
  • "error.stack is the live call stack." It is a string saved when the error was made, for debugging. Each engine writes it in its own way.
  • "Handler tables are part of JavaScript." They are one way an engine makes the language rules work. Your code can't see them.

These four ideas sound alike. Read each row as: the idea, what it is, and what it is not.

Ideas that are easy to mix up
IdeaWhat it isWhat it is not
A language ruleHow throw, try, catch, finally, and promise rejections behave in every engine.Handler tables, step numbers, or V8's optimization bits.
An engine detailHow V8 stores handlers and optimizes code that has them.A promise that every engine stores the exact same table.
A stack traceA string saved on an Error object for debugging, in many engines.The live call stack, or the steps the engine takes after a throw.
A rejected promiseA promise that failed. await can throw its error inside an async function.A throw right away that an older try around a callback could catch later.

Practice exercises

6 EXERCISES
Exercise 1 · Warm-upPredict what eval gives back

eval runs a string as code and gives back the value of the last statement. What does this line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
console.log(eval("try { 1 } finally { 2 }"));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upSpot the finally return

    Type the string that this code prints.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function demo() {
      try { return "try"; }
      finally { return "finally"; }
    }
    console.log(demo());

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWhich line prints first?

      These are the last two lines of the await example in "Errors that happen later". Which message prints first?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      submitUpiPayment();
      console.log("after submitUpiPayment call");

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the timer bug

        This outer catch never runs. Where should the try/catch go so the timer's error is handled?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        try {
          setTimeout(() => { throw new Error("late"); }, 0);
        } catch (error) {
          console.log("caught");
        }

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · PracticeName the calls that leave the stack

          In the checkout example, which two calls leave the stack before placeOrder catches the error?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeUse it in a real app

            Name one rule from this lesson that you would use on a checkout or payment page.

            Answer, then press Check. Spacing and letter case don’t matter.

              Quiz: check your understanding

              8 QUESTIONS

              For each question, first ask how the code ends: normally, with a return, a throw, or a break, in a timer callback, or with an awaited rejection.

              Exceptions and unwinding quiz · 8 questionsScore: first tries count
              1. Question 1 of 8In V8, what does the handler table of a function with try and catch do?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does this code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function demo() {
                  try {
                    return "try";
                  } finally {
                    return "finally";
                  }
                }
                console.log(demo());

                Choose an answer to see the explanation.

              3. Question 3 of 8An error leaves validateUpi, then chargeCard, and then reaches placeOrder. What is the engine doing?

                Choose an answer to see the explanation.

              4. Question 4 of 8What does this line print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log(eval("try { 1 } finally { 2 }"));

                Choose an answer to see the explanation.

              5. Question 5 of 8Which sentence about try/catch speed is true for today's V8?

                Choose an answer to see the explanation.

              6. Question 6 of 8What does this timer code print first?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                try {
                  setTimeout(() => console.log("timer"), 0);
                  console.log("after scheduling");
                } catch (error) {
                  console.log("caught");
                }

                Choose an answer to see the explanation.

              7. Question 7 of 8How can a try/catch inside an async function catch a rejected promise?

                Choose an answer to see the explanation.

              8. Question 8 of 8Which habit fits this lesson?

                Choose an answer to see the explanation.

              Key takeaways

              • In V8, a function with try/catch has a handler table. It says where to jump when a guarded line throws.
              • After a throw, the engine checks the current call, runs any finally it must, and takes the call off the stack. It moves outward until a catch handles the error.
              • A finally block always runs when its code is left. A return or throw in finally replaces what was happening before.
              • Modern V8 can optimize functions with try/catch/finally. The slow part is throwing: making the error, recording the stack trace, and recovering.
              • A normal try/catch can't catch errors in timer callbacks or in promises you don't await. await brings a rejection back into the async function.

              Remember the one-liner.
              A throw leaves calls until it finds a catch, runs cleanup on the way out, and is meant for rare problems, not normal work.

              Next, Why garbage collection? moves from the call stack to memory. You'll see how JavaScript finds objects that nothing uses any more, and frees them.

              CompleteFrontend Clear concepts. Working examples.