Skip to main content

Command Palette

Search for a command to run...

Error Handling in JS: Try, Catch, Finally

Updated
5 min readView as Markdown

We have all been there. You spend hours writing a beautiful, complex feature. It works perfectly on your machine. You deploy it to production, a user clicks a button, and suddenly—nothing happens. The screen freezes. The UI breaks.

You open the developer console and there it is: a terrifying block of bright red text. Uncaught TypeError: Cannot read properties of undefined.

In JavaScript, when an unexpected event occurs (like a server going down, a missing piece of data, or a simple typo), the JavaScript engine throws an error. If you don't actively catch and handle that error, your entire script stops executing dead in its tracks.

This is where the art of Error Handling comes in. Instead of letting your application explode, you can engineer it to fail gracefully. Let's look at how to use try, catch, and finally to keep your apps running smoothly, even when things go wrong.

The Problem: Unhandled Runtime Errors

Imagine you are building a weather dashboard that fetches data from an external API and processes it.

// A risky operation
const weatherData = JSON.parse(apiResponse); // What if apiResponse is broken or empty?
console.log(`The temperature is ${weatherData.temp}`);
console.log("This will never run if the line above fails.");

If apiResponse isn't properly formatted JSON, JSON.parse() will panic and throw an error. Because there is no safety net, the code halts immediately. The user is left staring at a broken screen, and that final console.log never sees the light of day.

The Safety Net: try and catch

To prevent these catastrophic crashes, JavaScript gives us the try...catch statement. It allows you to test a block of code for errors, and if one occurs, it hands control over to a rescue block instead of killing the program.

try {
  // 1. Put your risky code in here
  const weatherData = JSON.parse(apiResponse); 
  console.log(`The temperature is ${weatherData.temp}`);
  
} catch (error) {
  // 2. If ANYTHING fails in the try block, execution jumps here
  console.error("Oops! We couldn't read the weather data.");
  console.error("The exact error was:", error.message);
  
  // You can show a friendly fallback UI to the user here!
}

// 3. The program continues running normally!
console.log("Moving on to other tasks...");

How it works:

  1. The engine attempts to run the code inside the try block.

  2. If it succeeds, the catch block is completely ignored.

  3. If it fails, it instantly stops the try block and jumps into the catch block, passing along an error object that contains valuable debugging details (like error.message and error.stack).

The Guarantee: The finally Block

Sometimes, you have cleanup tasks that absolutely must happen, regardless of whether your code succeeded or failed.

For example, if you show a "Loading..." spinner before fetching data, you need to hide that spinner when you are done. If you only hide it at the end of the try block, and an error occurs, the spinner will stay on the screen forever.

Enter the finally block.

showLoadingSpinner();

try {
  const data = fetchSensitiveData();
  renderData(data);
} catch (error) {
  showErrorMessage("Failed to load data.");
} finally {
  // This will run NO MATTER WHAT happens above
  hideLoadingSpinner();
  console.log("Cleanup complete.");
}

The finally block is your guarantee. It is the perfect place to close database connections, clear out temporary variables, or reset UI states.

Taking Control: Throwing Custom Errors

Catching errors is only half the battle. Sometimes, the JavaScript engine thinks everything is fine, but your specific business logic dictates that an error has occurred.

For example, what if a user tries to withdraw more money than they have in their account? The math (100 - 500 = -400) is perfectly valid JavaScript, so the engine won't care. But your banking app definitely should!

You can create your own custom roadblocks using the throw keyword combined with the new Error() constructor.

function processWithdrawal(balance, amount) {
  if (amount > balance) {
    // We intentionally trigger an error
    throw new Error("Insufficient funds for this transaction.");
  }
  
  if (amount <= 0) {
    throw new Error("Withdrawal amount must be greater than zero.");
  }

  return balance - amount;
}

try {
  const newBalance = processWithdrawal(100, 500);
} catch (error) {
  // Our custom error messages will be caught here
  alert(`Transaction Failed: ${error.message}`); 
}

By aggressively throwing custom errors, you "fail fast." You stop bad data from flowing deeper into your application and causing mysterious bugs later on.

Why Error Handling is Non-Negotiable

Implementing strong error handling separates amateur scripts from professional software:

  • Superior User Experience (Graceful Failure): Instead of a frozen screen, you can show the user a polite message: "We're having trouble reaching the server right now, please try again."

  • Drastically Easier Debugging: By logging the error.stack inside your catch blocks, you get a precise map pointing directly to the file and line number where the code broke.

  • Application Stability: Your application becomes resilient. One failed API call or unexpected null value won't bring down the entire system.

Code is inherently messy because the real world is unpredictable. APIs go down, users input weird characters, and network connections drop. By embracing try, catch, finally, and throw, you accept that unpredictability and build applications tough enough to handle it.