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.
- try contains code that might throw an error.
- catch runs only if an error is thrown in try.
- finally runs after try and catch, whether an error happened or not.
- It is used for runtime errors, not syntax errors.
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
- Parsing JSON from an API response or local storage.
- Handling user input that may not meet the expected format.
- Wrapping code that throws custom errors from your own functions.
- Cleaning up resources in finally, such as temporary state or locks.
- Adding context to low-level errors before passing them to a caller.
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
- try / catch does not catch syntax errors in the file itself.
- It only handles errors thrown in the same synchronous call stack unless you await a promise.
- finally still runs if return or throw happens inside try or catch.
- If finally throws its own error, that new error can replace the earlier one.
- Be careful not to silently ignore errors, especially in data processing or authentication code.
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
- try contains code that might throw at runtime.
- catch handles the error only when one is thrown.
- finally always runs for cleanup tasks.
- Use await inside try when handling promise rejections.
- Do not use try / catch as a replacement for input validation.
11. Practice Exercise
- Write a function that accepts a string and tries to parse it as JSON.
- If parsing fails, return null and log a helpful message.
- Always print a cleanup message in finally.
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.