JavaScript error types
The Error object
Before we can handle errors properly, we need to understand what they actually are. In JavaScript, every error is an object. It is not a string. It is not a number. It is an object with specific properties that tell you what went wrong and where.
Let’s look at what an error looks like up close:
try {
throw new Error("Something went wrong");
} catch (err) {
console.log(err.message); // "Something went wrong"
console.log(err.name); // "Error"
console.log(err.stack); // "Error: Something went wrong\n at ..."
} Three properties matter here.
message is the human-readable description. This is the string you pass to new Error("..."). It should tell you what happened.
name is the error type. For a basic error it is just "Error", but for more specific errors it might be "TypeError" or "RangeError". This is the class name of the error.
stack is where things get really useful for debugging. It is a string that shows exactly where the error was created: the function name, the file, and the line number for every call in the stack. This is essential for tracking down bugs. But remember from the last lesson: never send this to the client. Stack traces are for your logs, not for your API responses.
Built-in error types
JavaScript comes with several error types built in. You do not need to memorize all of them, but you should recognize the ones you will see most often.
Error is the base class. It is the generic, catch-all error. When nothing more specific fits, you use Error.
TypeError is the most common error you will see at runtime. It means a value is not the type you expected:
const user = undefined;
user.name; // TypeError: Cannot read properties of undefined (reading 'name')
const fn = "not a function";
fn(); // TypeError: fn is not a function That first example should look familiar. It is the exact error from the previous lesson, where we tried to access user.name when the database returned undefined. TypeError is telling you “you tried to do something with a value, but that value does not support that operation.”
RangeError happens when a value is outside the allowed range:
new Array(-1); // RangeError: Invalid array length You cannot create an array with a negative length. Makes sense.
SyntaxError means the code is not valid JavaScript. You usually see this at parse time (before your code even runs), but you can also hit it at runtime when parsing data:
JSON.parse("not json"); // SyntaxError: Unexpected token 'n' This one comes up a lot in APIs. If a client sends a request body that is not valid JSON, JSON.parse throws a SyntaxError. Hectoday HTTP handles JSON parsing for you when you define a body schema, so you will not deal with this directly. But it is good to know what happens under the hood.
ReferenceError means you used a variable that does not exist:
console.log(unknownVariable); // ReferenceError: unknownVariable is not defined This is almost always a typo or a scoping issue in your code.
What throw does
We have been using throw without really explaining what it does. When you throw an error, two things happen. First, execution stops immediately at that line. Nothing after throw runs. Second, the error propagates up the call stack, going from function to function, looking for a catch block. If it finds one, the catch block handles the error. If it does not find one, the process crashes.
function getUser(id: string) {
const user = db.prepare("SELECT * FROM users WHERE id = ?").get(id);
if (!user) throw new Error("User not found"); // Execution stops here
return user; // This line never runs if user is undefined
}
function handleRequest() {
const user = getUser("nonexistent"); // Error propagates to here
return Response.json(user); // This line never runs
}
// If no one catches the error, the process crashes The error starts in getUser, propagates up to handleRequest, then to whoever called handleRequest, and so on. If nothing catches it anywhere in the chain, Node.js prints the error and exits. This is exactly the crash scenario from the first lesson.
Now, should you actually throw here? Not always. Later in this course, we will learn when it makes more sense to return an error response directly instead of throwing. Throwing is best reserved for truly unexpected situations. When a user requests a product that does not exist, that is not unexpected. It is a normal part of how the app gets used. We will explore this idea further once we cover structured error handling.
You can throw anything (but should you?)
Here is something interesting about JavaScript: you can technically throw any value. A string, a number, an object, anything. But just because you can does not mean you should.
// BAD: throwing a string
throw "Something went wrong";
// No stack trace. No .name. Cannot use instanceof.
// BAD: throwing a plain object
throw { code: 404, message: "Not found" };
// No stack trace. Cannot use instanceof.
// GOOD: throwing an Error
throw new Error("Something went wrong");
// Has message, name, stack. Can use instanceof. What do you think happens when you throw a string and try to read err.stack in the catch block? You get undefined. The stack trace is gone. Without a stack trace, you have no idea where the error came from. You just know “something went wrong” somewhere in your codebase. That is a nightmare to debug.
If you do throw, always use new Error() or a class that extends Error. That way the stack trace is preserved and debugging stays possible.
The cause property
ES2022 added a useful feature: the cause property. It lets you wrap one error inside another.
Why would you want that? Imagine a payment fails because of a network error. The low-level error says something like “ECONNREFUSED 10.0.0.5:443.” That is technical and meaningless to anyone except the person debugging the network. You want to throw a higher-level error that says “Payment failed,” but you also want to keep the original error for debugging.
try {
await fetch("https://api.payment.com/charge");
} catch (err) {
throw new Error("Payment failed", { cause: err });
} Now the outer error has a clean message (“Payment failed”) and the cause property holds the original error with all its technical details and stack trace:
catch (err) {
console.log(err.message); // "Payment failed"
console.log(err.cause.message); // "fetch failed" or network error details
} You get the best of both worlds. Clean messages for humans, detailed context for debugging. We will use this pattern throughout the course.
Exercises
Exercise 1: Create an Error, a TypeError, and a SyntaxError. Log the name, message, and stack of each.
Exercise 2: Throw a string vs throw an Error. Catch both. Compare what err.stack contains. (The string has no stack trace.)
Exercise 3: Use the cause property to wrap a low-level error in a higher-level one. Log both the outer message and the inner cause.
Next, we will learn where to actually catch these errors, because catching in the wrong place is just as bad as not catching at all.
Why should you always throw Error objects instead of strings or plain objects?