JSON in JavaScript
An error is something that goes wrong while your code is running. JavaScript automatically creates an error object and throws it, which stops your code from continuing unless you handle it.
Errors are not always your fault. An API could return unexpected data, a file could be missing, a user could type something your code did not expect. Good error handling means your app stays alive and gives the user a useful message instead of crashing silently.
Every error in JavaScript is an instance of the built-in Error class. An error object has two key properties:
- name — the type of error (e.g. "TypeError", "ReferenceError")
- message — a human-readable description of what went wrong
- stack — a stack trace showing where the error originated (very useful for debugging)
try {
null.toString(); // null has no methods — this throws a TypeError
} catch (e) {
console.log(e.name); // TypeError
console.log(e.message); // Cannot read properties of null (reading 'toString')
console.log(e instanceof TypeError); // true
console.log(e instanceof Error); // true — all error types extend Error
}
The try...catch statement is the main tool for handling errors in JavaScript. Code inside the try block runs normally. If an error is thrown, JavaScript jumps to the catch block instead of crashing.
try {
// Code that might fail
const data = JSON.parse("this is not valid json");
} catch (e) {
// Code that runs only when an error occurs
console.log("Something went wrong:", e.message);
}
// Execution continues here regardless
console.log("Done");
The catch block receives the error object. You can name it anything — e, err, error are all common. Use it to log, display, or recover from the problem:
function parseUserInput(input) {
try {
const result = JSON.parse(input);
return result;
} catch (err) {
console.error("Invalid JSON input:", err.message);
return null; // return a safe default instead of crashing
}
}
console.log(parseUserInput('{"name": "Alice"}')); // { name: 'Alice' }
console.log(parseUserInput("bad data")); // null — error handled
The finally block runs no matter what — whether the try block succeeded, whether an error was thrown, or even if there was a return statement in the try or catch block.
Use it for cleanup work that must always happen — closing a database connection, hiding a loading spinner, releasing a lock:
function fetchData(url) {
console.log("Loading...");
try {
// Simulate a fetch that might fail
if (!url) throw new Error("URL is required");
console.log("Fetched:", url);
} catch (e) {
console.error("Fetch failed:", e.message);
} finally {
console.log("Loading complete."); // always runs
}
}
fetchData("https://api.example.com"); // Fetched, then Loading complete
fetchData(""); // Fetch failed, then Loading complete
finally is optional. You can have try...catch, try...finally, or try...catch...finally. The catch is also optional, but you must have either catch or finally.
Watch out: If finally itself throws an error or has a return statement, it overrides any value returned from try or catch. Keep finally focused on cleanup only.The throw statement lets you create and throw your own error. You can throw it from deep inside a function and it will bubble up the call stack until something catches it.
You can technically throw anything — a string, a number, an object. But the strong convention is to always throw an Error object (or a subclass of it), because that gives you the name, message, and stack properties:
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 (e) {
console.error(e.message); // Cannot divide by zero
}
Use throw inside validation functions to signal that input is bad, rather than returning null or false and hoping the caller checks it:
function createUser(name, age) {
if (typeof name !== "string" || name.trim() === "") {
throw new TypeError("name must be a non-empty string");
}
if (typeof age !== "number" || age < 0 || age > 150) {
throw new RangeError("age must be a number between 0 and 150");
}
return { name, age };
}
try {
createUser("", 25); // throws TypeError
} catch (e) {
console.log(e.name); // TypeError
console.log(e.message); // name must be a non-empty string
}
JavaScript has several built-in error types. Each one extends the base Error class and signals a different kind of problem:
- Error — the generic base. Use it when no more specific type fits.
- SyntaxError — invalid JavaScript syntax (e.g., broken JSON in JSON.parse()).
- ReferenceError — accessing a variable that does not exist.
- TypeError — wrong type used (e.g., calling a non-function, accessing property on null).
- RangeError — a numeric value is outside the allowed range (e.g., invalid array length).
- URIError — malformed URI in decodeURIComponent().
- EvalError — related to eval(). Rarely seen in practice.
// ReferenceError — variable not declared
try {
console.log(undeclaredVar);
} catch (e) {
console.log(e.name); // ReferenceError
}
// TypeError — wrong type
try {
null.toString();
} catch (e) {
console.log(e.name); // TypeError
}
// RangeError — out of range
try {
new Array(-1);
} catch (e) {
console.log(e.name); // RangeError
}
// SyntaxError — invalid JSON
try {
JSON.parse("{bad json}");
} catch (e) {
console.log(e.name); // SyntaxError
}
In a catch block, you can check the error type to decide how to handle it:
try {
riskyOperation();
} catch (e) {
if (e instanceof TypeError) {
console.log("Type problem:", e.message);
} else if (e instanceof RangeError) {
console.log("Range problem:", e.message);
} else {
throw e; // re-throw errors you don't know how to handle
}
}
You can create your own error classes by extending Error. This lets you define domain-specific errors with meaningful names, making your catch blocks cleaner and your intent clearer.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = "DatabaseError";
this.query = query; // add custom properties
}
}
// Usage
try {
throw new ValidationError("Email is required");
} catch (e) {
console.log(e.name); // ValidationError
console.log(e.message); // Email is required
console.log(e instanceof ValidationError); // true
console.log(e instanceof Error); // true
}
Custom errors are especially useful in larger applications. Instead of checking e.message with string comparisons, you check e instanceof YourErrorClass — which is much more reliable:
function processForm(data) {
if (!data.email) throw new ValidationError("Email is required");
if (!data.name) throw new ValidationError("Name is required");
// ... save to DB
}
try {
processForm({ name: "Alice" }); // missing email
} catch (e) {
if (e instanceof ValidationError) {
console.log("Form error:", e.message); // Form error: Email is required
} else {
throw e; // unexpected — re-throw
}
}
When an error is thrown and not caught locally, JavaScript propagates it up the call stack — from the function where it happened, to the function that called it, and so on, until something catches it or it reaches the top level and crashes the program.
function level3() {
throw new Error("Something broke in level 3");
}
function level2() {
level3(); // no try/catch here — error bubbles up
}
function level1() {
try {
level2();
} catch (e) {
console.log("Caught in level1:", e.message);
// Caught in level1: Something broke in level 3
}
}
level1();
A common pattern is to catch an error, do something (like log it), and then re-throw it so the caller still knows something went wrong. This is useful when you can partially handle an error but not fully:
function loadConfig(path) {
try {
// attempt to read config
return JSON.parse(readFile(path));
} catch (e) {
if (e instanceof SyntaxError) {
console.error("Config file has invalid JSON:", e.message);
throw e; // re-throw — the caller needs to know this failed
}
// For other errors (file not found etc.), silently return defaults
return {};
}
}
- An error is an object with name, message, and stack properties. All error types extend the base Error class.
- try...catch: code in try runs normally. If it throws, control jumps to catch. Execution continues after the block either way.
- finally: always runs regardless of success or failure. Use it for cleanup code that must execute no matter what.
- throw: create and throw your own errors. Always throw an Error instance (or subclass), not a raw string.
- Error types: TypeError, ReferenceError, RangeError, SyntaxError, and others. Use instanceof in catch to handle specific types differently.
- Custom errors: extend Error to create domain-specific errors. Set this.name in the constructor to give it a meaningful name.
- Error propagation: uncaught errors bubble up the call stack. Catch only what you can handle. Re-throw errors you cannot fully handle.
- What is the difference between try...catch and try...catch...finally?
- When does the finally block run?
- What types of errors does try...catch not catch?
- What is the difference between TypeError and ReferenceError?
- What does throw do and what should you throw?
- How do you create a custom error class in JavaScript?
- What is error propagation and how does it work?
- When should you re-throw an error instead of handling it?
- How do you handle errors in asynchronous code?
- What is the difference between e.message and e.stack?