Error Handling (try/catch/finally) Get Certified
Errors are inevitable. Good programs do not crash when something
goes wrong — they detect the problem, recover when possible, and
leave useful information for debugging. JavaScript’s primary tool
for this is the try...catch...finally statement.
Basic try…catch
Code that might fail goes inside the try block.
If an error is thrown, control jumps to the catch
block.
What this code does: Attempts to parse invalid JSON. The error is caught and a friendly message is logged instead of crashing.
console.log("Attempting to parse JSON...");
try {
const data = JSON.parse("this is not valid JSON");
console.log(data);
} catch (error) {
console.log("Caught an error:", error.message);
}
console.log("Program continues normally");
console.log("Attempting to parse JSON...")try { — place code that might throw here.const data = JSON.parse("this is not valid JSON") — invalid JSON throws a SyntaxError.console.log(data) — never reached when parse fails.} catch (error) { — receives the thrown Error object.console.log("Caught an error:", error.message) — program continues instead of crashing.}console.log("Program continues normally") — the key benefit of try/catch: recovery instead of a hard stop.Throwing Your Own Errors
You can raise an error yourself with the throw
keyword. Always throw an actual Error instance
(or a subclass) so you get a useful stack trace.
What this code does: Manually throws an Error when an invalid value is detected, then catches and reports it.
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
console.log("Trying division...");
try {
console.log(divide(10, 0));
} catch (error) {
console.log("Error:", error.message);
}
console.log("Finished");
function divide(a, b)if (b === 0) { throw new Error("Cannot divide by zero"); } — throw immediately exits the function and jumps to the nearest catch.return a / b}console.log("Trying division...")try {console.log(divide(10, 0))} catch (error) {console.log("Error:", error.message)}console.log("Finished")The finally Block
The finally block always runs after
try and catch, whether an error
occurred or not. It is ideal for cleanup work.
What this code does: Demonstrates that finally runs both
on success and on failure.
function runWithCleanup(shouldFail) {
try {
console.log("Working...");
if (shouldFail) {
throw new Error("Something went wrong");
}
console.log("Success");
} catch (error) {
console.log("Caught:", error.message);
} finally {
console.log("Cleanup always runs");
}
}
console.log("--- Success case ---");
runWithCleanup(false);
console.log("--- Failure case ---");
runWithCleanup(true);
function runWithCleanup(shouldFail)try {console.log("Working...")if (shouldFail) { throw new Error("Something went wrong"); }console.log("Success")} catch (error) {console.log("Caught:", error.message)} finally { — always runs after try or catch, even if you return or throw again.console.log("Cleanup always runs") — ideal for closing files, hiding spinners, releasing locks.} }runWithCleanup(false) — try → Success → finally.runWithCleanup(true) — try → throw → catch → finally. Same finally in both paths.Error Properties
Every Error object has three useful properties:
name, message, and stack.
What this code does: Creates an Error and logs its name, message, and a shortened version of the stack.
try {
throw new Error("Something broke");
} catch (error) {
console.log("name:", error.name);
console.log("message:", error.message);
console.log("stack (first line):", error.stack.split("\\n")[0]);
}
try {throw new Error("Something broke") — creates an Error with name, message, and stack.} catch (error) {console.log("name:", error.name) — usually “Error” for the base type.console.log("message:", error.message) — the string you passed to the constructor.console.log("stack (first line):", error.stack.split("\n")[0]) — call stack snapshot; invaluable for debugging.}Built-in Error Types
JavaScript provides several specialized error constructors.
What this code does: Demonstrates TypeError, RangeError, and ReferenceError.
// TypeError
try {
null.foo();
} catch (e) {
console.log("TypeError:", e.message);
}
// RangeError
try {
new Array(-1);
} catch (e) {
console.log("RangeError:", e.message);
}
// ReferenceError
try {
console.log(undeclaredVariable);
} catch (e) {
console.log("ReferenceError:", e.message);
}
// TypeErrornull.foo() — TypeError: cannot read property of null.// RangeErrornew Array(-1) — RangeError: invalid array length.// ReferenceErrorconsole.log(undeclaredVariable) — ReferenceError: not defined. Knowing the type helps choose the right recovery.Creating Custom Error Types
You can create your own error types by extending the built-in
Error class. This is useful when you want to
distinguish between different kinds of application-specific
errors.
What this code does: Defines a custom ValidationError
that extends Error, then throws it when an invalid
age is supplied.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function setAge(age) {
if (age < 0) {
throw new ValidationError("Age cannot be negative");
}
return age;
}
console.log("Setting age...");
try {
setAge(-1);
} catch (error) {
console.log(error.name + ":", error.message);
}
class ValidationError extends Error — subclass so you can detect this failure type with instanceof.constructor(message) {super(message) — sets message and stack.this.name = "ValidationError" — otherwise name would still be "Error".} }function setAge(age)if (age < 0) { throw new ValidationError("Age cannot be negative"); }return age}console.log("Setting age...")try { setAge(-1); } catch (error) { console.log(error.name + ":", error.message); }Catching Specific Error Types
Inside a catch block, you can use
instanceof to check which kind of error was
thrown and respond differently for each one.
What this code does: Throws different error types and uses
instanceof in the catch block to handle each case.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function setAge(age) {
if (typeof age !== "number") {
throw new TypeError("Age must be a number");
}
if (age < 0) {
throw new ValidationError("Age cannot be negative");
}
return age;
}
console.log("Testing validation...");
try {
setAge(-1);
} catch (error) {
if (error instanceof ValidationError) {
console.log("Validation problem:", error.message);
} else if (error instanceof TypeError) {
console.log("Type problem:", error.message);
} else {
console.log("Unexpected error:", error.message);
}
}
class ValidationError extends Error { ... this.name = "ValidationError"; }function setAge(age)if (typeof age !== "number") { throw new TypeError("Age must be a number"); }if (age < 0) { throw new ValidationError("Age cannot be negative"); }return age}console.log("Testing validation...")try {setAge(-1)} catch (error) {if (error instanceof ValidationError) { ... } — different recovery for different error kinds.else if (error instanceof TypeError) { ... }else { console.log("Unexpected error:", error.message); }Rethrowing Errors
Sometimes you want to handle part of an error (such as logging
it) but still let it propagate further up. You can do this by
throwing the error again inside the catch block.
What this code does: Catches an error at a lower level, logs it, then re-throws so an outer handler can also react.
function riskyOperation() {
throw new Error("Low-level failure");
}
function process() {
try {
riskyOperation();
} catch (error) {
console.log("Logged at process():", error.message);
throw error; // rethrow
}
}
console.log("Starting...");
try {
process();
} catch (error) {
console.log("Caught at top level:", error.message);
}
function riskyOperation()throw new Error("Low-level failure")}function process()try {riskyOperation()} catch (error) {console.log("Logged at process():", error.message) — handle what you can (logging), then pass the error up.throw error — without this, the outer catch would never see the failure.} }console.log("Starting...")try { process(); } catch (error) { console.log("Caught at top level:", error.message); } — rethrow lets higher layers decide the final response.Nested try...catch Blocks
A try...catch statement can be placed inside
another one, allowing you to handle errors at different levels.
What this code does: Shows nested try...catch blocks.
The inner catch handles the error so the outer try continues.
try {
console.log("Outer try starts");
try {
throw new Error("Inner failure");
} catch (innerError) {
console.log("Handled inside:", innerError.message);
}
console.log("Outer try continues normally");
} catch (outerError) {
console.log("Outer catch:", outerError.message);
}
try {console.log("Outer try starts")try {throw new Error("Inner failure")} catch (innerError) {console.log("Handled inside:", innerError.message) — error is fully handled here, so it does not reach the outer catch.}console.log("Outer try continues normally") — because the inner catch did not rethrow.} catch (outerError) {console.log("Outer catch:", outerError.message)}Optional Catch Binding
If you do not need the error object itself, you can omit the
parentheses and parameter after catch.
What this code does: Uses a catch clause without a binding when the error details are not needed.
try {
JSON.parse("not valid json");
console.log("This will not run");
} catch {
console.log("Invalid JSON — using default value instead.");
}
try {JSON.parse("not valid json")console.log("This will not run")} catch { — no binding when you do not need the error object (ES2019+).console.log("Invalid JSON — using default value instead.")}Error Handling with async/await
try...catch also works with
async/await. A rejected promise
thrown by await is caught the same way as a
synchronous error.
What this code does: Awaits a Promise that rejects. The rejection is
caught by the surrounding try...catch.
function loadData() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
reject(new Error("Failed to load data"));
}, 80);
});
}
async function run() {
try {
console.log("Loading...");
const data = await loadData();
console.log(data);
} catch (error) {
console.log("Caught:", error.message);
} finally {
console.log("Attempt finished");
}
}
run();
console.log("run() called");
function loadData()return new Promise(... reject(new Error("Failed to load data")) ...)async function run()try {console.log("Loading...")const data = await loadData() — rejected Promise becomes a thrown exception inside async functions.console.log(data)} catch (error) {console.log("Caught:", error.message)} finally {console.log("Attempt finished")} }run()console.log("run() called") — same try/catch/finally pattern works for both sync and async code.Common Built-in Error Types
| Error Type | When It Is Thrown |
|---|---|
| TypeError | A value is used in a way its type does not allow |
| RangeError | A value falls outside an allowed range |
| ReferenceError | A reference is made to an undeclared variable |
| SyntaxError | Code or data (like JSON) is malformed |
| Error | The general-purpose base type for custom errors |
Common Mistakes with Error Handling
- Wrapping too much code in one
tryblock, making it unclear which line actually failed. - Catching an error and doing nothing with it (an empty
catchblock), hiding real problems. - Throwing plain strings or objects instead of an
Errorinstance, losing thestackproperty. - Forgetting that
finallyalways runs, even after areturninsidetryorcatch. - Not checking the specific error type when different errors need different handling.
What this code does: Contrasts the bad practice of throwing a plain
string with the recommended practice of throwing a real
Error instance.
// Prefer throwing an Error instance:
function goodExample() {
throw new Error("Something went wrong");
}
try {
goodExample();
} catch (error) {
console.log("Caught properly:", error.message);
}
// Prefer throwing an Error instance:function goodExample()throw new Error("Something went wrong") — gives you name, message, and stack. Plain strings do not.}try {goodExample()} catch (error) {console.log("Caught properly:", error.message)}Best Practices for Error Handling
- Keep
tryblocks small and focused on the code that can actually fail. - Always throw
Errorinstances (or subclasses), never plain strings or numbers. - Use custom error classes to distinguish between different failure types in larger applications.
- Never leave a
catchblock empty; at minimum, log the error. - Use
finallyfor cleanup that must run regardless of success or failure. - Only catch errors you can actually handle; let unexpected ones propagate or be logged centrally.
Quick Summary
tryruns code that might fail;catchhandles the error if one occurs.finallyalways runs, whether an error occurred or not.throwraises an error manually, usually as anErrorinstance.- Every error has a
name, amessage, and astack. - Built-in error types include
TypeError,RangeError,ReferenceError, andSyntaxError. - Custom error classes can extend
Errorfor application-specific errors. try...catchalso works withasync/await.
Try It Yourself
Modify the code below. Try changing the age to a negative number, passing a string instead of a number, or adding your own custom error class.
What this code does: A complete practice example that validates age,
throws typed errors, distinguishes them with instanceof,
and always runs the finally block.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function setAge(age) {
if (typeof age !== "number") {
throw new TypeError("Age must be a number");
}
if (age < 0) {
throw new ValidationError("Age cannot be negative");
}
return age;
}
try {
console.log("Setting age...");
console.log(setAge(28));
} catch (error) {
if (error instanceof ValidationError) {
console.log("Validation problem:", error.message);
} else if (error instanceof TypeError) {
console.log("Type problem:", error.message);
} else {
console.log("Unexpected error:", error.message);
}
} finally {
console.log("Done setting age");
}
class ValidationError extends Error { ... }function setAge(age)if (typeof age !== "number") { throw new TypeError(...); }if (age < 0) { throw new ValidationError(...); }return age}try {console.log("Setting age...")console.log(setAge(28)) — succeeds; try changing to -1 or "28" to exercise the branches.} catch (error) {if (error instanceof ValidationError) { ... }else if (error instanceof TypeError) { ... }else { ... }} finally {console.log("Done setting age")}Tip: Keeptryblocks small, always throw realErrorobjects, never leave acatchblock empty, and usefinallyfor cleanup that has to run no matter what happens.
Understanding try, catch, and
finally ties together everything you have learned
about promises, async/await, and the Fetch API, giving you the
tools to write JavaScript that fails gracefully instead of
crashing.
🧪 Test Your JavaScript Code
Edit the code on the left and click "Run Code" to see the console output on the right.
Click "Run Code" to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.