DEV Community

Cover image for Error Handling in JavaScript: Try, Catch, Finally
Pratham
Pratham

Posted on

Error Handling in JavaScript: Try, Catch, Finally

How to write code that breaks gracefully instead of crashing spectacularly.


Here's what happens when your JavaScript code hits an error and you haven't handled it:

const user = undefined;
console.log(user.name);
// TypeError: Cannot read properties of undefined (reading 'name')
// 💥 Script stops. Everything below this line NEVER runs.
Enter fullscreen mode Exit fullscreen mode

The entire program crashes. Any code after the error? Gone. UI update below it? Never happens. API call scheduled later in the file? Cancelled. One unhandled error takes down everything.

That's not how production software should work. Users submit empty forms, APIs go down, JSON comes back malformed, variables end up undefined when you expected an object. Errors will happen. The question is: does your code crash, or does it handle the situation gracefully?

This is what try, catch, and finally are for. I learned this lesson the hard way in the ChaiCode Web Dev Cohort 2026 — let me save you the pain.


What Are Errors in JavaScript?

An error is something that goes wrong during code execution. JavaScript stops at that point and throws an Error object — a built-in object that contains information about what went wrong.

Common Runtime Errors

// 1. ReferenceError — using a variable that doesn't exist
console.log(myVariable);
// ReferenceError: myVariable is not defined

// 2. TypeError — using a value the wrong way
const num = 42;
num.toUpperCase();
// TypeError: num.toUpperCase is not a function

// 3. SyntaxError — code that JavaScript can't parse
// (caught before execution, can't be caught with try/catch)
// const x = ;  ← SyntaxError

// 4. RangeError — value outside allowed range
const arr = new Array(-1);
// RangeError: Invalid array length

// 5. URIError — malformed URI
decodeURIComponent("%");
// URIError: URI malformed
Enter fullscreen mode Exit fullscreen mode

The Error Object

Every error has three useful properties:

try {
  undefinedVariable;
} catch (error) {
  console.log(error.name); // "ReferenceError"
  console.log(error.message); // "undefinedVariable is not defined"
  console.log(error.stack); // Full stack trace with file/line numbers
}
Enter fullscreen mode Exit fullscreen mode
  • name — the type of error (TypeError, ReferenceError, etc.)
  • message — a human-readable description of what went wrong
  • stack — the full call stack at the time of the error (invaluable for debugging)

Using try and catch Blocks

The try/catch statement lets you attempt risky code and handle any errors that occur — without crashing the program.

Basic Syntax

try {
  // Code that might throw an error
} catch (error) {
  // Code that runs if an error was thrown
}
Enter fullscreen mode Exit fullscreen mode

Example: Without Error Handling

const data = JSON.parse("this is not valid JSON");
console.log(data);
console.log("This line never runs"); // 💥 Script crashed above
Enter fullscreen mode Exit fullscreen mode

Example: With Error Handling

try {
  const data = JSON.parse("this is not valid JSON");
  console.log(data);
} catch (error) {
  console.log("Parsing failed:", error.message);
}
console.log("This line STILL runs! ✅"); // Program continues
Enter fullscreen mode Exit fullscreen mode

Output:

Parsing failed: Unexpected token 't', "this is no"... is not valid JSON
This line STILL runs! ✅
Enter fullscreen mode Exit fullscreen mode

The error was caught. The program didn't crash. Life goes on.

How It Works

try {
  // JavaScript TRIES to run this code
  // If everything is fine → catch is SKIPPED
  // If an error occurs → execution JUMPS to catch immediately
}
catch (error) {
  // Only runs if try threw an error
  // The error object contains info about what went wrong
}
// Code here continues running either way
Enter fullscreen mode Exit fullscreen mode

More Realistic Examples

// Safely accessing nested properties
try {
  const user = { name: "Pratham", address: null };
  const city = user.address.city; // address is null!
} catch (error) {
  console.log("Could not read address:", error.message);
  // "Could not read address: Cannot read properties of null (reading 'city')"
}

// Safely parsing API responses
try {
  const response = '{"name": "Pratham", "age": 22}';
  const user = JSON.parse(response);
  console.log(`User: ${user.name}, Age: ${user.age}`);
} catch (error) {
  console.log("Invalid JSON:", error.message);
}
Enter fullscreen mode Exit fullscreen mode

The finally Block

finally is the third piece. It runs no matter what — whether try succeeded or catch was triggered.

Syntax

try {
  // Risky code
} catch (error) {
  // Handle the error
} finally {
  // This ALWAYS runs — success or failure
}
Enter fullscreen mode Exit fullscreen mode

Example

function readConfig(jsonString) {
  console.log("⏳ Attempting to parse config...");

  try {
    const config = JSON.parse(jsonString);
    console.log(`✅ Theme: ${config.theme}, Language: ${config.language}`);
  } catch (error) {
    console.log(`❌ Failed to parse config: ${error.message}`);
  } finally {
    console.log("🏁 Config parsing attempt complete.");
  }
}

// Success case
readConfig('{"theme": "dark", "language": "en"}');
// ⏳ Attempting to parse config...
// ✅ Theme: dark, Language: en
// 🏁 Config parsing attempt complete.

// Failure case
readConfig("not json");
// ⏳ Attempting to parse config...
// ❌ Failed to parse config: Unexpected token 'o', "not json" is not valid JSON
// 🏁 Config parsing attempt complete.
Enter fullscreen mode Exit fullscreen mode

finally ran in both cases. That's its superpower.

When to Use finally

  • Hiding loading spinners — show "Loading..." before the try, hide it in finally
  • Closing connections — database or file connections that should close regardless
  • Cleaning up resources — removing temporary data, resetting state
  • Logging — recording that an operation was attempted, no matter the outcome
async function fetchData(url) {
  let isLoading = true;
  console.log("Loading...");

  try {
    const response = await fetch(url);
    const data = await response.json();
    console.log("Data received:", data.title);
  } catch (error) {
    console.log("Fetch failed:", error.message);
  } finally {
    isLoading = false;
    console.log("Loading complete. isLoading:", isLoading);
  }
}
Enter fullscreen mode Exit fullscreen mode

Try → Catch → Finally Execution Order

┌─────────────────────────────────────────────────────┐
│                                                     │
│                  try { ... }                        │
│                      │                              │
│              ┌───────┴────────┐                     │
│              ↓                ↓                     │
│        No error?         Error thrown?              │
│              ↓                ↓                     │
│        Skip catch       catch (error) { ... }      │
│              │                │                     │
│              └───────┬────────┘                     │
│                      ↓                              │
│              finally { ... }                        │
│              (ALWAYS runs)                          │
│                      ↓                              │
│              Code after try/catch/finally           │
│              continues normally                     │
│                                                     │
└─────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Three Possible Paths

// Path 1: No error
try {
  console.log("A"); // ✅ runs
  console.log("B"); // ✅ runs
} catch (error) {
  console.log("C"); // ❌ skipped
} finally {
  console.log("D"); // ✅ always runs
}
// Output: A, B, D

// Path 2: Error at line A
try {
  undefinedVar; // 💥 error — jumps to catch
  console.log("B"); // ❌ skipped (never reached)
} catch (error) {
  console.log("C"); // ✅ runs
} finally {
  console.log("D"); // ✅ always runs
}
// Output: C, D

// Path 3: try without catch (just finally)
try {
  console.log("A"); // ✅ runs
} finally {
  console.log("D"); // ✅ always runs
}
// Output: A, D
Enter fullscreen mode Exit fullscreen mode

Throwing Custom Errors

JavaScript lets you create and throw your own errors using the throw statement. This is powerful for enforcing rules in your code.

Basic Custom Throw

function divide(a, b) {
  if (b === 0) {
    throw new Error("Cannot divide by zero!");
  }
  return a / b;
}

try {
  console.log(divide(10, 2)); // 5
  console.log(divide(10, 0)); // 💥 throws!
} catch (error) {
  console.log("Error:", error.message);
  // "Error: Cannot divide by zero!"
}
Enter fullscreen mode Exit fullscreen mode

Validating User Input

function createUser(name, age) {
  if (!name || name.trim() === "") {
    throw new Error("Name is required!");
  }
  if (typeof age !== "number" || age < 0) {
    throw new Error("Age must be a positive number!");
  }
  if (age < 13) {
    throw new Error("User must be at least 13 years old!");
  }

  return { name: name.trim(), age, createdAt: new Date() };
}

try {
  const user1 = createUser("Pratham", 22);
  console.log("Created:", user1);
  // Created: { name: "Pratham", age: 22, createdAt: ... }

  const user2 = createUser("", 25);
  // 💥 throws "Name is required!"
} catch (error) {
  console.log("Validation failed:", error.message);
  // "Validation failed: Name is required!"
}
Enter fullscreen mode Exit fullscreen mode

Throwing Specific Error Types

function processAge(age) {
  if (typeof age !== "number") {
    throw new TypeError("Age must be a number!");
  }
  if (age < 0 || age > 150) {
    throw new RangeError("Age must be between 0 and 150!");
  }
  return `Age is valid: ${age}`;
}

try {
  console.log(processAge(22)); // "Age is valid: 22"
  console.log(processAge("twenty")); // throws TypeError
} catch (error) {
  if (error instanceof TypeError) {
    console.log("Type problem:", error.message);
  } else if (error instanceof RangeError) {
    console.log("Range problem:", error.message);
  } else {
    console.log("Unknown error:", error.message);
  }
}
Enter fullscreen mode Exit fullscreen mode

Using instanceof lets you handle different error types differently — like routing errors to different recovery strategies.


Why Error Handling Matters

1. Your App Doesn't Crash

Without error handling, one error kills the entire script. With it, the error is contained and the rest of your code keeps running.

// ❌ Without handling — everything stops
const data = JSON.parse(invalidJSON); // crash
updateUI(); // never runs
saveToDatabase(); // never runs

// ✅ With handling — app continues
try {
  const data = JSON.parse(invalidJSON);
} catch (e) {
  showFallbackUI();
}
updateUI(); // still runs
saveToDatabase(); // still runs
Enter fullscreen mode Exit fullscreen mode

2. Users See Friendly Messages

// ❌ User sees: "TypeError: Cannot read properties of undefined"
// 😱 Confusing and scary

// ✅ User sees: "Something went wrong. Please try again."
// 😌 Professional and helpful
Enter fullscreen mode Exit fullscreen mode

3. Debugging Becomes Easier

When you catch errors properly, you can log detailed information for developers while showing simple messages to users:

try {
  riskyOperation();
} catch (error) {
  // For developers (in console/logs)
  console.error(`[${error.name}] ${error.message}`);
  console.error(error.stack);

  // For users (on screen)
  showMessage("Something went wrong. Please try again.");
}
Enter fullscreen mode Exit fullscreen mode

4. Graceful Degradation

Instead of a broken page, you show fallback content:

async function loadProfile() {
  try {
    const response = await fetch("/api/profile");
    const profile = await response.json();
    displayProfile(profile);
  } catch (error) {
    // API is down? Show cached data instead.
    const cached = localStorage.getItem("profile");
    if (cached) {
      displayProfile(JSON.parse(cached));
      showNotice("Showing cached data — couldn't reach the server.");
    } else {
      showError("Unable to load your profile. Please check your connection.");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The Benefits — Summarized

Without Error Handling With Error Handling
App crashes on any error App continues running
User sees cryptic error messages User sees friendly messages
No information about what failed Detailed logs for debugging
Broken state left behind Resources cleaned up in finally
All-or-nothing execution Graceful degradation with fallbacks

Let's Practice: Hands-On Assignment

Part 1: Basic Try/Catch

function safeParse(jsonString) {
  try {
    const result = JSON.parse(jsonString);
    console.log("Parsed successfully:", result);
    return result;
  } catch (error) {
    console.log("Invalid JSON:", error.message);
    return null;
  }
}

safeParse('{"name": "Pratham"}'); // Parsed successfully: { name: "Pratham" }
safeParse("not json at all"); // Invalid JSON: ...
safeParse(""); // Invalid JSON: Unexpected end of JSON input
Enter fullscreen mode Exit fullscreen mode

Part 2: Custom Error Throwing

function withdraw(balance, amount) {
  if (amount <= 0) {
    throw new Error("Amount must be positive!");
  }
  if (amount > balance) {
    throw new Error("Insufficient funds!");
  }
  return balance - amount;
}

try {
  console.log(withdraw(1000, 500)); // 500
  console.log(withdraw(1000, 1500)); // throws!
} catch (error) {
  console.log("Transaction failed:", error.message);
  // "Transaction failed: Insufficient funds!"
}
Enter fullscreen mode Exit fullscreen mode

Part 3: Try/Catch/Finally Together

function processData(data) {
  console.log("⏳ Processing started...");

  try {
    if (!data) throw new Error("No data provided!");

    const parsed = JSON.parse(data);
    console.log(`✅ Processed: ${parsed.name}`);
    return parsed;
  } catch (error) {
    console.log(`❌ Error: ${error.message}`);
    return null;
  } finally {
    console.log("🏁 Processing complete (cleanup done).");
  }
}

processData('{"name": "Pratham"}');
// ⏳ Processing started...
// ✅ Processed: Pratham
// 🏁 Processing complete (cleanup done).

processData(null);
// ⏳ Processing started...
// ❌ Error: No data provided!
// 🏁 Processing complete (cleanup done).
Enter fullscreen mode Exit fullscreen mode

Part 4: Error Handling with Async/Await

async function fetchUser(userId) {
  try {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/users/${userId}`,
    );

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const user = await response.json();
    console.log(`✅ User: ${user.name} (${user.email})`);
  } catch (error) {
    console.log(`❌ Failed: ${error.message}`);
  } finally {
    console.log("🏁 Fetch attempt complete.");
  }
}

fetchUser(1); // Success
fetchUser(9999); // Might fail depending on API
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Errors are runtime problems — TypeError, ReferenceError, RangeError, etc. Without handling, they crash your entire script.
  2. try/catch lets you attempt risky code and handle failures gracefully. The program continues instead of crashing.
  3. finally runs no matter what — success or failure. Use it for cleanup: hiding spinners, closing connections, resetting state.
  4. throw lets you create custom errors for input validation, business rules, or any situation where you want to signal a problem explicitly.
  5. Error handling isn't optional — it's what separates a script that works in ideal conditions from software that works in the real world.

Wrapping Up

Errors aren't bugs — they're expected. APIs fail. Users type garbage. JSON gets corrupted. The mark of a good developer isn't writing code that never errors — it's writing code that handles errors gracefully. try/catch/finally is your toolbox for exactly that.

I'm building this mindset through the ChaiCode Web Dev Cohort 2026 under Hitesh Chaudhary and Piyush Garg. Error handling became critical the moment we started working with fetch, async/await, and form validation — basically, the moment our code started interacting with the real world. Get comfortable with it now and your apps will be rock solid.

Connect with me on LinkedIn or visit PrathamDEV.in. More articles on the way as I keep building and learning.

Happy coding! 🚀


Written by Pratham Bhardwaj | Web Dev Cohort 2026, ChaiCode

Top comments (0)