JavaScript Error Handling: try, catch, finally and throw
Learn JavaScript error handling with try, catch, finally and throw. Understand error types, create custom errors, and handle problems in real projects.
What you will learn
Things go wrong in every real program: a user types bad data, a file is missing, the internet drops, a server sends broken text. If you do nothing, JavaScript throws an error and stops running the code. In this lesson you will learn what errors are, the different error types, how to catch them with try...catch, how to clean up with finally, how to create your own errors with throw, how to build custom error classes, and how to use all of this in real projects, so your website shows a friendly message instead of simply breaking.
What happens when there is an error?
console.log("Step 1");
const user = null;
console.log(user.name); // TypeError: Cannot read properties of null
console.log("Step 3"); // never runs
The error at the middle line stops the program. “Step 3” is never printed. Error handling lets you decide what happens instead.
The Error object
When something goes wrong, JavaScript creates an Error object with useful information:
| Property | Meaning |
|---|---|
name |
The type of error, e.g. "TypeError" |
message |
A text explaining what happened |
stack |
The list of function calls that led to the error (great for debugging) |
cause |
Optional: the original error that caused this one |
const error = new Error("Something went wrong");
console.log(error.name); // "Error"
console.log(error.message); // "Something went wrong"
console.log(typeof error.stack); // "string"
Common error types
| Type | When it happens | Example |
|---|---|---|
SyntaxError |
The code or the data is written wrongly | JSON.parse("{ bad") |
ReferenceError |
You use a variable that does not exist | console.log(missingVariable) |
TypeError |
You use a value in a way its type does not allow | null.name, 5() |
RangeError |
A number is outside the allowed range | new Array(-1), (1).toFixed(101) |
URIError |
Wrong use of URI functions | decodeURIComponent("%") |
Let us trigger and catch each one safely:
function tryIt(label, action) {
try {
action();
} catch (error) {
console.log(`${label}: ${error.name}`);
}
}
tryIt("Reference", () => console.log(missingVariable)); // ReferenceError
tryIt("Type", () => { const x = null; x.name; }); // TypeError
tryIt("Syntax", () => JSON.parse("{ bad json")); // SyntaxError
tryIt("Range", () => new Array(-1)); // RangeError
tryIt("URI", () => decodeURIComponent("%")); // URIError
Note: a normal
SyntaxErrorin your own code (like a missing bracket) stops the whole file before it runs, so you cannot catch it. You can only catch syntax errors from things that run later, such asJSON.parse.
try...catch
The idea is simple: try some risky code, and if it throws an error, catch it and handle it.
try {
// risky code
} catch (error) {
// runs only if an error was thrown
}
try {
console.log("Before the error");
const data = JSON.parse("{ bad json");
console.log("This line is skipped");
} catch (error) {
console.log("Something went wrong:", error.message);
}
console.log("The program keeps running!");
How it works:
- The code in
tryruns line by line. - If an error happens, the rest of the
tryblock is skipped. - JavaScript jumps to
catchand gives you the error object. - After
catch, the program continues normally.
If no error happens, the catch block is skipped.
Optional catch binding
If you do not need the error object, you can leave it out:
try {
JSON.parse("{ bad");
} catch {
console.log("Invalid data");
}
Only the code inside try is protected
try {
const a = 10;
console.log(a.toUpperCase()); // TypeError, caught
} catch (error) {
console.log("Caught:", error.name);
}
// An error out here would NOT be caught
Keep the try block small: wrap only the risky lines, not your whole program.
finally
The finally block runs always, whether there was an error or not. It is perfect for cleanup.
function readData(text) {
console.log("Loading...");
try {
return JSON.parse(text);
} catch (error) {
console.log("Invalid data");
return null;
} finally {
console.log("Done loading"); // runs in every case
}
}
readData('{"ok": true}');
// Loading...
// Done loading
readData("{ bad");
// Loading...
// Invalid data
// Done loading
Notice that finally runs even after a return. The return value is kept, and finally runs just before the function really leaves.
Typical uses of finally: hide a loading spinner, close a connection, re-enable a button, release a resource.
You can write try...finally without catch. The error then continues upwards after the finally block runs.
throw: create your own errors
Sometimes the code is valid, but the situation is wrong: a negative age, an empty name, a missing item. You can raise an error yourself with throw.
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
console.log(divide(10, 2)); // 5
try {
divide(10, 0);
} catch (error) {
console.log(error.message); // "Cannot divide by zero"
}
When throw runs, the function stops immediately, just like a return, and the error travels up to the nearest catch.
Always throw Error objects
JavaScript allows you to throw anything, but you should always throw an Error object, because it has a stack that shows where the problem started.
// Avoid:
// throw "Something went wrong";
// throw 404;
// Good:
// throw new Error("Something went wrong");
You can also throw the built-in specific types when they fit:
function setAge(age) {
if (typeof age !== "number") {
throw new TypeError("Age must be a number");
}
if (age < 0 || age > 120) {
throw new RangeError("Age must be between 0 and 120");
}
return age;
}
try {
setAge("twenty");
} catch (error) {
console.log(error.name, "-", error.message); // TypeError - Age must be a number
}
How errors travel up
If a function does not catch an error, it moves up to the function that called it, then to the one that called that, and so on, until something catches it. If nothing does, the program stops.
function level3() {
throw new Error("Problem in level 3");
}
function level2() {
level3();
}
function level1() {
try {
level2();
} catch (error) {
console.log("Caught in level 1:", error.message);
}
}
level1(); // Caught in level 1: Problem in level 3
This is useful: low-level functions throw, and one place near the top catches and decides what to show the user.
Rethrowing: catch only what you understand
A catch block catches every error, even bugs you did not expect. A good habit: handle the errors you know, and throw the rest again.
function parseAge(text) {
try {
const value = JSON.parse(text);
return value.age;
} catch (error) {
if (error instanceof SyntaxError) {
return null; // we expected this one
}
throw error; // unknown problem: do not hide it
}
}
console.log(parseAge('{"age": 22}')); // 22
console.log(parseAge("{ bad")); // null
Custom errors
For bigger projects, create your own error types by extending Error. Then you can tell them apart with instanceof.
class ValidationError extends Error {
constructor(message, field) {
super(message); // sets the message
this.name = "ValidationError";
this.field = field; // extra information
}
}
class NotFoundError extends Error {
constructor(item) {
super(`${item} was not found`);
this.name = "NotFoundError";
}
}
Using them:
function createUser(name, email) {
if (!name) throw new ValidationError("Name is required", "name");
if (!email.includes("@")) throw new ValidationError("Email is not valid", "email");
return { name, email };
}
try {
createUser("Riya", "riya-at-example.com");
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Problem with "${error.field}": ${error.message}`);
} else {
throw error;
}
}
// Problem with "email": Email is not valid
(Classes are explained fully in the OOP section. For now, copy the pattern: extends Error, call super(message), and set this.name.)
Error cause: keep the original error
When you catch an error and throw a more friendly one, keep the original with the cause option:
function loadSettings(text) {
try {
return JSON.parse(text);
} catch (error) {
throw new Error("Could not load settings", { cause: error });
}
}
try {
loadSettings("{ bad");
} catch (error) {
console.log(error.message); // "Could not load settings"
console.log(error.cause.name); // "SyntaxError"
}
Errors in asynchronous code (a preview)
try...catch works only for code that runs right now. It cannot catch an error that happens later inside a callback:
try {
setTimeout(() => {
throw new Error("Too late to catch");
}, 100);
} catch (error) {
console.log("This never runs");
}
The error above happens 100 ms later, after the try block has finished. For asynchronous code you will use .catch() on Promises and try...catch with async/await. Here is a small preview:
// You will learn this in the Asynchronous JavaScript lessons
async function loadUser() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const user = await response.json();
console.log(user.name);
} catch (error) {
console.log("Could not load the user:", error.message);
} finally {
console.log("Request finished");
}
}
loadUser();
Notice the check on response.ok. fetch does not throw for a 404 or 500 reply. It throws only when the network itself fails. So you must throw the error yourself.
Global safety nets
For errors nobody caught, browsers send an event. You can use it to log the problem (not to hide it):
window.addEventListener("error", (event) => {
console.log("Uncaught error:", event.message);
});
window.addEventListener("unhandledrejection", (event) => {
console.log("Unhandled promise error:", event.reason);
});
Real websites send these messages to a logging service so the developer can fix the bug.
Real-life use cases
1. Safe JSON parsing with a fallback
function safeParse(text, fallback = null) {
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
console.log(safeParse('{"a": 1}')); // { a: 1 }
console.log(safeParse("not json", [])); // []
2. Reading from localStorage safely
function getSaved(key, fallback) {
try {
const raw = localStorage.getItem(key);
return raw === null ? fallback : JSON.parse(raw);
} catch {
return fallback; // broken data or storage blocked by the browser
}
}
console.log(getSaved("cart", []));
3. Form validation with clear messages
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validateSignup({ name, age, password }) {
if (!name?.trim()) throw new ValidationError("Please enter your name", "name");
if (!Number.isInteger(age) || age < 13) throw new ValidationError("You must be 13 or older", "age");
if (!password || password.length < 8) throw new ValidationError("Password needs 8+ characters", "password");
return true;
}
try {
validateSignup({ name: "Riya", age: 22, password: "abc" });
} catch (error) {
if (error instanceof ValidationError) {
console.log(`[${error.field}] ${error.message}`); // [password] Password needs 8+ characters
}
}
4. Loading indicator with finally
function simulateLoad(shouldFail) {
const status = { loading: true, error: null };
console.log("Show spinner");
try {
if (shouldFail) throw new Error("Network problem");
console.log("Show data");
} catch (error) {
status.error = error.message;
console.log("Show error message:", status.error);
} finally {
status.loading = false;
console.log("Hide spinner"); // always hidden, success or failure
}
}
simulateLoad(false);
simulateLoad(true);
5. Retry an action
function unreliableTask(attempt) {
if (attempt < 3) throw new Error(`Failed on attempt ${attempt}`);
return "Success!";
}
function withRetry(task, maxAttempts) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return task(attempt);
} catch (error) {
console.log(error.message);
if (attempt === maxAttempts) throw error; // give up after the last try
}
}
}
console.log(withRetry(unreliableTask, 5));
// Failed on attempt 1
// Failed on attempt 2
// Success!
6. A safe wrapper that returns a result instead of throwing
function attempt(fn) {
try {
return { ok: true, value: fn() };
} catch (error) {
return { ok: false, error };
}
}
const result = attempt(() => JSON.parse('{"name":"Riya"}'));
if (result.ok) {
console.log(result.value.name); // "Riya"
} else {
console.log("Failed:", result.error.message);
}
7. Guard clauses: check inputs early
function calculateDiscount(price, percent) {
if (typeof price !== "number" || Number.isNaN(price)) {
throw new TypeError("Price must be a number");
}
if (percent < 0 || percent > 100) {
throw new RangeError("Percent must be between 0 and 100");
}
return price - (price * percent) / 100;
}
console.log(calculateDiscount(1000, 20)); // 800
8. User-friendly messages
Never show raw technical errors to visitors. Show a friendly message, and log the technical details for yourself:
function showFriendlyError(error) {
console.error(error); // technical details for the developer
if (error instanceof SyntaxError) return "The data we received was not valid.";
if (error instanceof TypeError) return "Something went wrong. Please try again.";
return "Unexpected problem. Please try again later.";
}
try {
JSON.parse("{ bad");
} catch (error) {
console.log(showFriendlyError(error)); // "The data we received was not valid."
}
Best practices
- Throw
Errorobjects, not text or numbers. - Keep
tryblocks small. Wrap only the risky lines. - Do not leave
catchempty. At least log the error withconsole.error. A silentcatchhides bugs. - Catch what you can handle. Rethrow the rest.
- Do not use exceptions for normal flow. Use an
iffor things you expect often (like an empty search box). - Do not rely on the exact
messagetext. It can differ between browsers. Checkerror.nameorinstanceof. - Use
finallyfor cleanup such as hiding spinners and re-enabling buttons. - Show friendly messages to users and keep technical details in the console or your logs.
Common mistakes
- Putting too much code in one
tryblock. You will not know which line failed. - An empty
catch {}. The error disappears and the bug becomes very hard to find. - Expecting
try...catchto catch errors in asetTimeoutor other callbacks. Handle the error inside the callback, or use Promises andasync/await. - Forgetting that
fetchdoes not throw for HTTP errors like 404 and 500. Checkresponse.ok. - Throwing text like
throw "error". You lose the stack trace anderror.message. - Forgetting
super(message)when creating a custom error class. - Forgetting to set
this.namein a custom error, so it shows as plainError. - Catching an error and then returning nothing, which makes the rest of the code fail with a confusing
undefined. - Comparing
error.messagetext to decide what to do. Useinstanceofwith custom errors instead. - Using
finallywith areturninside it, which overrides thereturnor error fromtryandcatch. Avoidreturninfinally. - Confusing errors with bugs.
try...catchhandles problems that can happen even in correct code (bad input, network). It does not replace fixing real bugs.
Practice
- Write code that tries to call
JSON.parse("{ name: 'Riya' }"), catches the error and printserror.nameand a friendly message. - Write a
try...catch...finallythat prints"start", then an error message, then"cleanup". - Write
divide(a, b)that throws anErrorwhenbis 0. Call it insidetry...catch. - Write
setAge(age)that throws aTypeErrorif the age is not a number and aRangeErrorif it is below 0 or above 120. - Create a
ValidationErrorclass with afieldproperty. Throw it from a function that checks a name and an email. - In a
catchblock, handleValidationErrorand rethrow every other error. - Write
safeParse(text, fallback)and test it with 3 good and 3 bad inputs. - Write a function
readNumber(text)that converts text to a number and throws an error when the result isNaN. - Predict the output of a function that has
return "try"insidetryandconsole.log("finally")insidefinally. Then run it and check. - Challenge: write
withRetry(task, attempts)that runstask, retries when it throws, and rethrows the last error after the final attempt.
Recap
- An uncaught error stops the program. Error handling lets you recover or show a friendly message.
- An
Errorobject hasname,message,stackand optionallycause. Common types:SyntaxError,ReferenceError,TypeError,RangeError,URIError. try { ... } catch (error) { ... }runs risky code and handles any error. Usecatch { ... }when you do not need the error object.finally { ... }always runs, even after areturn. Use it for cleanup.throw new Error("message")raises an error. Always throwErrorobjects (or subclasses), never plain text.- Errors travel up the call chain until something catches them. Catch what you can handle and rethrow the rest.
- Create custom errors with
class MyError extends Error, callingsuper(message)and settingthis.name. Check them withinstanceof. - Use
{ cause: error }to keep the original error inside a new one. try...catchdoes not catch errors from later callbacks. Use Promises orasync/awaitfor asynchronous code, and checkresponse.okwithfetch.- Never leave
catchempty, and never show raw technical errors to users. - Next you will learn strict mode: a safer way to run JavaScript that turns hidden mistakes into clear errors.