Learning path · Level 4 · Tutorial 8

Handle Failures with JavaScript Exceptions

Signal invalid operations with exceptions and handle recoverable failures without silently hiding programming errors.

  • Level: Beginner
  • Duration: 16 min
  • 7 steps
Start tutorial Back to the path
Handle Failures with JavaScript Exceptions

Step 1 of 7

Recognize an Interrupted Operation

See how an exception differs from an ordinary function result and interrupts the current synchronous execution path.

Return or interruption

Two ways an operation can end

A function normally completes by returning an ordinary result to its caller. The caller can then continue with its next statement.

An exception is different: it interrupts the current operation. Instead of providing an ordinary result, it changes the execution path immediately. The statements that were still waiting on that failing path do not run as if the operation had succeeded.

Normal return compared with interruption

A side-by-side flow diagram comparing a normal function return with an exception that interrupts execution and skips later statements.

A normal return comes back to the caller. An exception interrupts the pending synchronous path instead.

Follow the current path

What “unhandled” changes

Think of the library-loan simulation as one synchronous chain: a request starts, it calls the loan operation, and later statements wait for that call to finish.

If an exception is unhandled, JavaScript stops the current synchronous execution at that point. It does not continue through the remaining statements with a pretend failure result.

This does not mean the browser permanently stops. You can still inspect the console, edit your local files, save, refresh, and run a corrected script again.

Execution outcomes

Compare the two outcomes of one function call.

An exception is not an ordinary failure value returned from a function.
QuestionOrdinary returnUnhandled exception
What reaches the caller?A normal resultNo ordinary result
What happens next?Later synchronous statements can runPending synchronous statements are skipped
Does the operation finish normally?YesNo; it is interrupted
What happens to the browser?It remains usableIt remains usable after this run stops

Check your prediction

Trace the interruption

Read this synchronous execution trace:

  1. requestLoan begins.
  2. requestLoan calls createLoan.
  3. An exception occurs inside createLoan.
  4. requestLoan has a statement immediately after its call.
  5. The outer script has a statement immediately after its call to requestLoan.

Which statement correctly describes this unhandled path?

Step 2 of 7

Throw an Error with a Useful Message

Use a guard to create and throw an informative Error when a library loan cannot be created.

Two separate actions

Create an Error, then throw it

new Error(message) creates an Error object. By itself, that expression does not interrupt a function; the next statement can still run.

throw is the statement that transfers control away from the current operation. Use both together when createLoan(book) cannot honestly produce its normal loan message.

Creation is not interruption

A comparison showing that creating an Error stays in the function while throwing it exits the current operation and skips the normal return path.

An Error object becomes an exception only when it is thrown.

Guard an impossible loan

Run the available-book path

In index.html, keep the local script connection:

<script src="exceptions.js" defer></script>

Put the following complete example in exceptions.js. Save both files, refresh the browser, and view the browser console.

Guard the operation before its normal return

exceptions.js

javascript
const book = {
  title: "Ocean Atlas",
  copies: 2
};

function createLoan(book) {
  if (book.copies === 0) {
    throw new Error(
      `Cannot create a loan for ${book.title}: no copies available.`
    );
  }

  return `Loan created for ${book.title}.`;
}

const message = createLoan(book);
console.log(message);
Expected output
Loan created for Ocean Atlas.

With `copies: 2`, the normal message is logged. To observe the failing path, change only `copies` to `0`, save, and refresh; the normal return and its console log do not run.

Example

Why `return` is different

The guard checks the invalid case first. Its message names the operation (create a loan), the relevant book (Ocean Atlas), and the reason (no copies available).

Do not use return new Error(...) for this guard. That is an ordinary return value, so the function has not thrown an exception. Likewise, creating an Error without throw leaves execution able to continue.

Check the distinction

Identify the interrupting statement

Which line interrupts createLoan(book) when it runs?

Apply the guard

Complete the failure signal

Complete the unavailable-book path so the function cannot continue to its normal success result.

javascript
function createLoan(book) {
  if (book.copies === 0) {
      Error(
      `Cannot create a loan for ${book.title}: no copies available.`
    );
  }

  return `Loan created for ${book.title}.`;
}

Step 3 of 7

Follow an Exception Through Function Calls

Trace the two possible routes

An exception travels outward

When requestLoan(book) calls createLoan(book), an available book produces an ordinary result that returns to the caller. requestLoan can store that result in message and continue.

For an unavailable book, createLoan throws before it can return. With no handler in either function, the synchronous exception leaves createLoan, then propagates out through requestLoan. It does not supply a value for message, so statements waiting after the unsuccessful call are skipped.

The same nested calls therefore have two routes: normal return for an available book, or propagation for an unavailable book.

Return flow and propagation

Comparison of a normal return from createLoan to requestLoan and an exception that travels outward through requestLoan.

A normal result returns to its caller. An unhandled exception exits each caller on its way outward.

Observe separate runs

Run the available-book path

In your local setup, save this complete version as exceptions.js, refresh index.html, and inspect the browser console. With two copies, createLoan returns normally, so both later logging statements run.

javascript
const book = { title: "Ocean Atlas", copies: 2 };

function createLoan(book) {
  if (book.copies === 0) {
    throw new Error(
      `Cannot create a loan for ${book.title}: no copies available.`
    );
  }

  return `Loan created for ${book.title}.`;
}

function requestLoan(book) {
  const message = createLoan(book);
  console.log(message);
}

requestLoan(book);
console.log("Request finished.");
Expected output
Loan created for Ocean Atlas.
Request finished.

This is the browser-console output when `copies` is 2.

Run the unavailable-book path separately

Now run this separate unavailable-book version. Save it as exceptions.js and refresh index.html. The exception occurs before the normal return. Because it is unhandled, it exits both functions, so neither later console.log statement runs.

javascript
const book = { title: "Ocean Atlas", copies: 0 };

function createLoan(book) {
  if (book.copies === 0) {
    throw new Error(
      `Cannot create a loan for ${book.title}: no copies available.`
    );
  }

  return `Loan created for ${book.title}.`;
}

function requestLoan(book) {
  const message = createLoan(book);
  console.log(message);
}

requestLoan(book);
console.log("Request finished.");
Expected output
Uncaught Error: Cannot create a loan for Ocean Atlas: no copies available.

Verify the first error line. Browsers may add a source location and differently formatted diagnostic details after it.

What each independent run can reach

Each save-and-refresh action starts a separate synchronous run. The unavailable run does not prevent you from editing the file and refreshing again to observe the available run.

The functions are called in the same order; the outcome inside createLoan changes the remaining path.
Book copiescreateLoan outcomeLater statements
2Returns the loan messageBoth console.log statements run
0Throws an ErrorThe return and both later logging statements are skipped

Practice: order the propagation path

Trace the failing nested call

With copies set to 0 and no handler present, which option gives the correct execution order and identifies the skipped statements?

Step 4 of 7

Catch a Failure at the Request Boundary

Handle a propagated synchronous exception at the request boundary and continue clearly after the catch.

Choose a useful handling boundary

Handle at the request boundary

createLoan(book) may throw because it cannot create the loan. requestLoan(book) is a useful handling boundary: it knows that someone requested a loan and can clearly report that request failure.

Put try around the operation that may fail. If createLoan(book) returns normally, the remaining statements in try run. If it throws, the remaining try statements are skipped and control moves to catch (error).

The error binding is available only inside its catch block. After that catch finishes normally, execution resumes after the whole try/catch statement—not at the line where the exception was thrown.

Two possible execution paths

A diagram compares a normal return from createLoan to requestLoan with an exception that travels to catch in requestLoan, then both paths continue afterward.

A catch handles an exception at the caller that has enough context to report the failed request.

Catch and report the failure

Run the handled unavailable-book case

Create these two local files. With copies: 0, refresh index.html and inspect the browser console. The success log is inside try, so it cannot run on the failing path. The catch reports the Error message, then the checkpoint confirms that synchronous execution continued after the handled failure.

index.html

index.html

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Library Loans</title>
  </head>
  <body>
    <script src="exceptions.js" defer></script>
  </body>
</html>

exceptions.js

exceptions.js

javascript
const book = { title: "Ocean Atlas", copies: 0 };

function createLoan(book) {
  if (book.copies === 0) {
    throw new Error(`Cannot create a loan for ${book.title}: no copies available.`);
  }

  return `Loan created for ${book.title}.`;
}

function requestLoan(book) {
  try {
    const message = createLoan(book);
    console.log(message);
  } catch (error) {
    console.error("Loan not created:", error.message);
  }
}

requestLoan(book);
console.log("Request finished.");
Expected output
Loan not created: Cannot create a loan for Ocean Atlas: no copies available.
Request finished.

The first line may use the browser console's error styling because it comes from console.error().

Practice the try/catch structure

Build the request handler

Complete the request-level handler.

javascript
function requestLoan(book) {
   {
    const message = createLoan(book);
    console.log(message);
  }  {
    
  }
}

Apply and trace the handled path

Apply the complete handled-failure flow

For a zero-copy book, write requestLoan(book) with a narrowly scoped try/catch. It must call createLoan(book), log the returned message only on success, and report the failure with the caught error's message. Then, on a separate line, identify the continuation checkpoint statement that executes after requestLoan(book) completes.

Do not add a catch inside createLoan(book).

Write at least 140 characters (0/140).

Step 5 of 7

Recover Deliberately or Rethrow

Handle a known recoverable loan failure while allowing unrecognized exceptions to continue outward.

Recover only what you understand

Choose an honest response

A recoverable failure is a condition this boundary understands and can report honestly. In this simulation, a book with no copies is known: the request cannot create a loan, but the caller can tell the user why.

An unexpected exception is different. This function does not know how to respond safely to every possible failure, so it should preserve that exception for an outer caller rather than pretending the request succeeded.

Known versus unrecognized failures

A flow diagram shows createLoan throwing an error to requestLoan. The request boundary catches and reports a known condition, while an unknown error continues outward.

Use a catch when this boundary can make a truthful response. Otherwise, let the exception continue outward.

Handle the known case only

Mark a known failure and preserve the rest

Give the unavailable-book Error an application-defined code property. code is not a built-in Error feature; this application chooses its meaning. The catch compares that stable identifier instead of comparing a human-readable message.

For the known code, report the failure. For every other exception, use throw error; to preserve the same Error object and continue propagation.

A deliberate handling boundary

Replace the current contents of exceptions.js with this example, save it, refresh index.html, and view the browser console.

javascript
const book = { title: "Ocean Atlas", copies: 0 };

function createLoan(book) {
  if (book.copies === 0) {
    const error = new Error(`Cannot create a loan for ${book.title}: no copies available.`);
    error.code = "NO_COPIES";
    throw error;
  }

  return `Loan created for ${book.title}.`;
}

function requestLoan(book) {
  try {
    const message = createLoan(book);
    console.log(message);
  } catch (error) {
    if (error.code === "NO_COPIES") {
      console.error("Loan not created:", error.message);
    } else {
      throw error;
    }
  }

  console.log("Request finished.");
}

requestLoan(book);
Expected output
Loan not created: Cannot create a loan for Ocean Atlas: no copies available.
Request finished.

Console error styling varies by browser. The two messages appear in this execution order.

Warning

Avoid hidden failures

Do not use an empty catch block or log a success-like fallback after a failure. Both hide the fact that no loan was created and can conceal programming mistakes.

The continuation message means this request boundary finished handling a known recoverable failure. It does not mean the loan operation succeeded.

Do not hide what happened

Decide whether to recover

Match each situation to the most honest action at the request boundary.

Tap an item and then its matching pair.

Apply the decision and rethrow rule

Complete the honest handler

Complete the decision for the recognized condition and the branch that preserves an unclassified exception.

javascript
function requestLoan(book) {
  try {
    const message = createLoan(book);
    console.log(message);
  } catch (error) {
    if (error.code === ) {
      console.error("Loan not created:", error.message);
    } else {
      ;
    }
  }

  console.log("Request finished.");
}

Step 6 of 7

Clean Up with finally

Place temporary-state cleanup in finally so it runs for normal completion, a handled recoverable failure, and a propagated exception.

Use finally for shared cleanup

One cleanup path

A finally block is a shared location for cleanup. It runs after a try block finishes normally and also when that block exits because of an exception.

For a loan request, temporary processing state must end on every path. Set isProcessing to true when the request starts, then reset it during cleanup.

finally does not handle an exception and does not mean that a loan was created. It performs cleanup before execution either continues normally or an exception moves outward.

How cleanup joins the paths

Three execution paths show finally cleanup after success, after a handled failure, and after a rethrow before the exception propagates.

finally runs before normal continuation after a handled failure and before a rethrown exception leaves the function.

Observe the three outcomes

Run all three cleanup paths

Keep the index.html file from the earlier request-handler step, including <script src="exceptions.js" defer></script>. Replace the entire contents of exceptions.js with the complete example below.

Start with scenario set to "available", save, refresh, and inspect the browser console. Then run two separate checks: change scenario to "unavailable", save and refresh; finally change it to "unexpected", save and refresh. Each refresh starts a new synchronous run.

exceptions.js

This complete local file handles the known recoverable failure, rethrows an unclassified failure, and performs cleanup in every case.

javascript
const scenario = "available";

const book = { title: "Ocean Atlas", copies: 2 };
let isProcessing = false;

if (scenario === "unavailable") {
  book.copies = 0;
}

function createLoan(book) {
  if (book.copies === 0) {
    const error = new Error(
      `Cannot create a loan for ${book.title}: no copies available.`
    );
    error.code = "NO_COPIES";
    throw error;
  }

  return `Loan created for ${book.title}.`;
}

function requestLoan(book) {
  isProcessing = true;

  try {
    if (scenario === "unexpected") {
      throw new Error("Unexpected loan failure.");
    }

    const message = createLoan(book);
    console.log(message);
  } catch (error) {
    if (error.code === "NO_COPIES") {
      console.error("Loan not created:", error.message);
    } else {
      throw error;
    }
  } finally {
    isProcessing = false;
    console.log("Processing:", isProcessing);
  }

  console.log("Request finished.");
}

requestLoan(book);
Expected output
Loan created for Ocean Atlas.
Processing: false
Request finished.

This output is for the initial `"available"` scenario. Use the table below to check the two edited scenarios. For `"unexpected"`, the browser also shows uncaught-Error details whose formatting and stack lines vary by browser.

Actions and observable output

Each row is a separate save-and-refresh run. “Execution action” describes control flow; “observable console output” contains only what you can observe in the console.

Cleanup output occurs before normal continuation or before a rethrown exception propagates.
ScenarioExecution actionObservable console output, in orderAfter try/catch/finally
AvailableThe try path returns normally.Loan created for Ocean Atlas. → Processing: false → Request finished.The request finishes normally.
Unavailablecatch recognizes `NO_COPIES` as a recoverable failure.Loan not created: Cannot create a loan for Ocean Atlas: no copies available. → Processing: false → Request finished.The request finishes normally.
Unexpectedcatch rethrows the unclassified Error.Processing: false → browser reports an uncaught Error.`Request finished.` is skipped.

Let an exception propagate after cleanup

Clean up without catching

Use try/finally when a function has cleanup responsibility but has no meaningful recovery to perform. With no catch, an exception from createLoan(book) still propagates after cleanup.

This is an alternative requestLoan version for understanding propagation. Do not place it together with the previous requestLoan definition.

try/finally preserves propagation

If createLoan(book) throws, finally runs first. The same exception then continues outward, so the last console.log does not run.

javascript
function requestLoan(book) {
  isProcessing = true;

  try {
    const message = createLoan(book);
    console.log(message);
  } finally {
    isProcessing = false;
    console.log("Processing:", isProcessing);
  }

  console.log("Request finished.");
}

Warning

Do not replace the pending outcome

Keep cleanup focused on cleanup. Do not return a replacement value or throw a replacement exception from finally: either action can override the result or exception already in progress. Resetting temporary state and logging that reset are safe cleanup tasks here.

Practice placing cleanup

Place the processing reset

Complete the shared cleanup block that resets temporary processing state on every exit path.

javascript
function requestLoan(book) {
  isProcessing = true;

  try {
    const message = createLoan(book);
    console.log(message);
  } catch (error) {
    if (error.code === "NO_COPIES") {
      console.error("Loan not created:", error.message);
    } else {
      throw error;
    }
  }  {
    isProcessing = ;
    console.log("Processing:", isProcessing);
  }

  console.log("Request finished.");
}

Check the complete exit flow

Predict cleanup and continuation

For the same request handler, write the two-line cleanup block that resets the processing state and logs it, including its block keyword. Then give the observable console-output order for an available request, a recognized unavailable request, and an unclassified request. State what happens to the continuation checkpoint in the unclassified case and why.

Write at least 180 characters (0/180).

Step 7 of 7

Apply and Verify the Complete Failure Flow

Complete and verify a local library-loan simulation that succeeds honestly, handles a recoverable failure, preserves an unexpected exception, and cleans up in every case.

Build the complete local example

Set up the local files

Create a local folder with two files: index.html and exceptions.js. Open index.html in your browser, then use the browser console to inspect each run. The scenario value is the only value you will edit for the three checks.

index.html

Save this as index.html. The deferred script runs after the document is parsed.

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Library Loan Exceptions</title>
    <script src="exceptions.js" defer></script>
  </head>
  <body>
    <h1>Library loan simulation</h1>
  </body>
</html>

exceptions.js

Save this as exceptions.js. Start with the available scenario, then edit only the scenario value before each later save-and-refresh run.

javascript
const scenario = "available";

const book = { title: "Ocean Atlas", copies: 2 };
let isProcessing = false;

if (scenario === "unavailable") {
  book.copies = 0;
}

function createLoan(book) {
  if (book.copies === 0) {
    const error = new Error(
      `Cannot create a loan for ${book.title}: no copies available.`
    );
    error.code = "NO_COPIES";
    throw error;
  }

  return `Loan created for ${book.title}.`;
}

function requestLoan(book) {
  isProcessing = true;

  try {
    if (scenario === "unexpected") {
      throw new Error("Unexpected loan failure.");
    }

    const message = createLoan(book);
    console.log(message);
  } catch (error) {
    if (error.code === "NO_COPIES") {
      console.error("Loan not created:", error.message);
    } else {
      throw error;
    }
  } finally {
    isProcessing = false;
    console.log("Processing:", isProcessing);
  }

  console.log("Request finished.");
}

requestLoan(book);
Expected output
Loan created for Ocean Atlas.
Processing: false
Request finished.

This output is for `const scenario = "available";`. Browser formatting for uncaught errors can vary in the unexpected scenario.

Run and verify each outcome

Run one scenario at a time

Run the file three separate times. First use "available". Next change scenario to "unavailable", save, and refresh. Finally change it to "unexpected", save, and refresh. Each refresh starts a new synchronous run, so the propagated exception in the final run does not prevent you from observing the earlier runs.

Compare the three execution paths

A three-path diagram compares normal return, a caught unavailable-book exception, and an unexpected exception rethrown after cleanup.

All three paths reach finally; only the recognized recoverable failure reaches the continuation after a catch.

Verification matrix

Use this matrix to verify the essential result of each run. In the unexpected case, the browser also displays its normal uncaught-error details; you do not need to investigate those details yet.

Expected behavior after each save-and-refresh run
ScenarioMain console resultCleanup outputContinuation checkpoint
availableLoan created for Ocean Atlas.Processing: falseRuns
unavailableLoan not created: Cannot create a loan for Ocean Atlas: no copies available.Processing: falseRuns
unexpectedUncaught Error: Unexpected loan failure.Processing: falseSkipped

Summary

Responsibility check

  • throw interrupts the operation when it cannot complete honestly.
  • catch reports only the recoverable NO_COPIES condition.
  • throw error; preserves an unclassified Error and lets it propagate.
  • finally performs cleanup whether the request succeeds, is handled, or propagates an exception.

Complete the handling logic

Complete the key failure-flow statements

Complete the statements that classify the known failure, preserve other exceptions, and perform cleanup.

javascript
function createLoan(book) {
  if (book.copies === 0) {
    const error = new Error(
      `Cannot create a loan for ${book.title}: no copies available.`
    );
    error.code = ;
    throw error;
  }

  return `Loan created for ${book.title}.`;
}

function requestLoan(book) {
  isProcessing = true;

  try {
    const message = createLoan(book);
    console.log(message);
  } catch (error) {
     {
      console.error("Loan not created:", error.message);
    } else {
      
    }
  } finally {
    
    console.log("Processing:", isProcessing);
  }

  console.log("Request finished.");
}

Apply the complete flow

Verify the complete failure flow

After completing the local example, run all three scenarios again. In 4–6 sentences, report which scenario logs the success message, which one reports the recoverable failure and continues, and which one propagates and skips the continuation checkpoint. State the cleanup value you observed in every run. Then explain why suppressing the unclassified Error would be misleading.

Write at least 250 characters (0/250).

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate