JavaScript try / catch / finally: Handling Errors Safely

JavaScript’s try / catch / finally statement lets you handle runtime errors without crashing your program. It is the standard way to protect risky code, report problems clearly, and run cleanup logic no matter what happens.

Quick answer: Put code that might fail inside try, handle the error in catch, and place cleanup code in finally. If an error is thrown in try, execution jumps to catch; finally runs after that either way.

Difficulty: Beginner

You'll understand this better if you know: basic JavaScript statements, functions, and how runtime errors differ from syntax errors.

1. What Is try / catch / finally?

try / catch / finally is JavaScript’s built-in error-handling structure. It gives you a controlled way to react when code throws an exception instead of letting the error stop the program immediately.

This matters because many JavaScript failures are unavoidable at runtime: bad JSON, missing data, invalid user input, network issues, or a function that throws on purpose.

2. Why try / catch / finally Matters

Without error handling, a thrown exception can stop the current script, prevent later code from running, and leave resources in an unfinished state. With try / catch / finally, you can keep the app responsive and recover gracefully.

Use it when you expect a specific operation may fail and you want a fallback path. Do not use it as a substitute for normal validation. If you can detect invalid input before calling risky code, do that first.

Typical reasons to use it include parsing data, calling APIs, working with code that may throw, and cleaning up temporary state after a failure.

3. Basic Syntax or Core Idea

The structure is simple: start a guarded block with try, handle failures in catch, and optionally add finally for cleanup.

Minimal pattern

The example below shows the general shape of the statement.

try {
  // Code that may throw
  dangerousOperation();
} catch (error) {
  // Handle the error
  console.error(error);
} finally {
  // Cleanup code that always runs
  cleanup();
}

Here, catch receives the thrown value in error. The finally block is optional, but it is useful when you need to close files, clear timers, reset UI state, or release locks.

What each part does

try starts the protected region. If everything inside succeeds, JavaScript skips catch and continues after the whole statement. If something throws, execution jumps into catch. Then finally runs before control leaves the statement.

4. Step-by-Step Examples

Example 1: Handling invalid JSON

Parsing JSON is a common place where errors appear. If the string is not valid JSON, JSON.parse() throws a SyntaxError.

const input = '{"name": "Ava"}';

try {
  const user = JSON.parse(input);
  console.log(user.name);
} catch (error) {
  console.error('Invalid JSON:', error.message);
}

This pattern is useful when reading stored data, API responses, or user-provided text that should be structured JSON.

Example 2: Falling back to a default value

You can use catch to replace a failed operation with a safe default.

function parsePageSize(value) {
  try {
    return Number.parseInt(value, 10);
  } catch {
    return 20;
  }
}

In this example, the function returns a fallback value if parsing fails. Notice that a catch block can omit the error parameter when you do not need it.

Example 3: Running cleanup with finally

finally is useful when cleanup must happen regardless of success or failure.

let connectionOpen = false;

try {
  connectionOpen = true;
  console.log('Working with the resource...');
  throw new Error('Something went wrong');
} catch (error) {
  console.error(error.message);
} finally {
  connectionOpen = false;
  console.log('Resource closed');
}

This is the pattern to use when a resource must be cleaned up even if an error happens midway.

Example 4: Re-throwing after adding context

Sometimes you want to add context to an error and then pass it upward again.

function loadUserProfile(json) {
  try {
    return JSON.parse(json);
  } catch (error) {
    throw new Error(`Could not load profile: ${error.message}`);
  }
}

This keeps the original failure visible while making the error easier to understand at a higher level of the app.

5. Practical Use Cases

In real projects, try / catch / finally is often used at boundaries: inside data-loading functions, command handlers, and request processors.

6. Common Mistakes

Mistake 1: Expecting try / catch to handle syntax errors in the file

try / catch only handles errors that happen while the code runs. It cannot rescue a script that fails to parse before execution starts.

Problem: This file contains invalid syntax, so JavaScript cannot even start running the try block.

try {
  const message = 'hello'
  // missing closing brace below causes a parse failure
} catch (error) {
  console.error(error);
}

Fix: Make sure the file is valid JavaScript first. Use try / catch for runtime failures, not syntax mistakes.

try {
  const message = 'hello';
  console.log(message);
} catch (error) {
  console.error(error);
}

The corrected version works because the code can be parsed and the handler is available if a runtime error occurs.

Mistake 2: Forgetting that async errors need await inside try

When a promise rejects, the rejection is only caught if the awaited expression is inside the try block.

Problem: In this code, the promise rejection happens after the try block finishes, so the catch block never sees it.

try {
  const response = fetch('/api/data');
  console.log(response);
} catch (error) {
  console.error('Request failed');
}

Fix: Use await inside the try block so a rejected promise becomes a caught exception in the async function.

async function loadData() {
  try {
    const response = await fetch('/api/data');
    return await response.json();
  } catch (error) {
    console.error('Request failed', error);
  }
}

The fixed version works because the rejection is observed at the await point.

Mistake 3: Assuming finally replaces catch

finally is for cleanup, not error handling. It does not receive the error and should not be used to decide whether the operation failed.

Problem: This code tries to recover in finally, but the error is still unhandled because no catch block exists.

try {
  throw new Error('Network down');
} finally {
  console.log('Trying to recover');
}

Fix: Add a catch block for recovery logic, and keep finally for cleanup only.

try {
  throw new Error('Network down');
} catch (error) {
  console.error('Recovering from error:', error.message);
} finally {
  console.log('Cleanup complete');
}

The corrected version works because error handling and cleanup each have the right job.

7. Best Practices

Practice 1: Catch only where you can actually recover

If you cannot do anything useful with an error, let it propagate to a higher-level handler. This keeps your code honest and avoids hiding bugs.

function getParsedUser(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    throw error;
  }
}

This approach is better than swallowing the error because callers still know something went wrong.

Practice 2: Use finally for cleanup, not for branching logic

finally should contain code that must always run. Keep decision-making in try or catch.

let locked = false;

try {
  locked = true;
  // work
} finally {
  locked = false;
}

This keeps the cleanup reliable and easy to understand.

Practice 3: Keep the protected block as small as possible

Only place code that can reasonably fail inside try. Smaller blocks make it easier to identify where the error came from.

try {
  const data = JSON.parse(input);
  return data.profile.name;
} catch (error) {
  console.error('Could not read profile name', error);
  return 'Anonymous';
}

Smaller try blocks make debugging faster and reduce the chance of hiding unrelated failures.

8. Limitations and Edge Cases

A common “not working” complaint is that a rejected promise seems to bypass catch. In practice, the fix is usually to await the promise inside an async function.

9. Practical Mini Project

Let’s build a small validator that reads a JSON string, extracts a user name, and always reports cleanup work.

function readUserName(jsonText) {
  let status = 'starting';

  try {
    status = 'parsing';
    const user = JSON.parse(jsonText);

    if (!user.name) {
      throw new Error('User name is missing');
    }

    status = 'done';
    return user.name;
  } catch (error) {
    status = 'failed';
    console.error('Could not read user:', error.message);
    return null;
  } finally {
    console.log('Status:', status);
  }
}

console.log(readUserName('{"name":"Mia"}'));
console.log(readUserName('{"name":""}'));

This mini project shows all three parts working together: parsing in try, handling failures in catch, and logging cleanup in finally.

10. Key Points

11. Practice Exercise

Expected output: The function should return the parsed object for valid JSON, return null for invalid JSON, and always log that cleanup ran.

Hint: Use JSON.parse() inside try, handle the error in catch, and place the cleanup log in finally.

function safeParseJson(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    console.error('Invalid JSON:', error.message);
    return null;
  } finally {
    console.log('Cleanup finished');
  }
}

console.log(safeParseJson('{"ok":true}'));
console.log(safeParseJson('not valid json'));

12. Final Summary

try / catch / finally is the core JavaScript pattern for handling runtime errors safely. It lets you separate risky code, recovery logic, and cleanup so your programs fail in a controlled way instead of breaking unexpectedly.

Use try for operations that might throw, catch to respond to the failure, and finally for work that must always happen. Once you are comfortable with this structure, the next useful topic is JavaScript throw and custom errors, which let you create clearer failures for your own functions.