Errors and Debugging
Table of Contents
- Errors Are Information
- Reading an Error Message
- The Four Errors You Will Meet Most
- try, catch, and finally
- Throwing Your Own Errors
- Project: Validating the Contact Form
- Debugging with console
- The DevTools Debugger
- Common Beginner Bugs
- Try It Yourself
1. Errors Are Information
Every programmer sees errors every day, including people who have written code for twenty years. An error is not a sign that you are bad at programming. It is the computer telling you exactly what it could not do and, usually, where.
The skill to build in this chapter is calm reading. When something breaks, do not guess and do not change random lines. Read the message, find the line, and ask: what did I expect, and what happened instead?
There are two kinds of problems:
| Kind | When it appears | Example |
|---|---|---|
| Error (exception) | The program stops and the engine reports it | Using a variable that does not exist |
| Bug (logic error) | The program runs but gives the wrong answer | A total that is off by one |
Errors are easy: the computer points at them. Bugs are harder, because nothing complains. Both are handled with the tools below.
2. Reading an Error Message
Create a file broken.js and run it with node broken.js:
function showTotal(prices) {
return prices.reduce((sum, price) => sum + price, 0);
}
console.log(showTotal(undefined));
Node prints something like this (paths will differ on your machine):
/home/dara/broken.js:2
return prices.reduce((sum, price) => sum + price, 0);
^
TypeError: Cannot read properties of undefined (reading 'reduce')
at showTotal (/home/dara/broken.js:2:17)
at Object.<anonymous> (/home/dara/broken.js:5:13)
There are four parts:
- The location: file
broken.js, line 2, with a^under the problem. - The type:
TypeError. - The message:
Cannot read properties of undefined (reading 'reduce'). Read it as a sentence: "I tried to read.reducefrom something that wasundefined." - The stack trace: the list of function calls that led here.
The stack trace reads from newest to oldest. The top line is where the
error happened (inside showTotal, line 2). The next line is who called
it (the top-level code, line 5). To find the real cause, look at the call
on line 5: it passed undefined. The error appeared on line 2, but the
mistake was on line 5.
In the browser, the same information appears in red in the Console panel, and each line of the trace is a link that opens the code.
3. The Four Errors You Will Meet Most
JavaScript has several built-in error types. These four cover most of what beginners see:
| Error | Meaning | Typical cause |
|---|---|---|
| SyntaxError | The code is not valid JavaScript | Missing bracket, a stray =, bad JSON |
| ReferenceError | A name does not exist | Typo in a variable name, or using it outside its scope |
| TypeError | A value cannot be used that way | Calling a non-function, reading a property of null or undefined |
| RangeError | A number is outside the allowed range | new Array(-1), endless recursion |
Here is one of each:
// SyntaxError: Unexpected token '=' (the file will not run at all)
const = 5;
// ReferenceError: username is not defined
console.log(username);
// TypeError: Cannot read properties of null (reading 'name')
const user = null;
console.log(user.name);
// RangeError: Invalid array length
const items = new Array(-1);
A SyntaxError is special: JavaScript reads the whole file before running it, so a syntax error stops everything and no line runs. The other three happen while the program runs, so earlier lines may already have executed.
TypeError: Cannot read properties of undefined is by far the most common
error on the web. It means a value you expected to exist is missing. In
Arrays and Objects
you saw optional chaining, user?.name, which returns undefined instead
of throwing.
4. try, catch, and finally
Sometimes an error is expected. A user might type bad data, or a file might be missing. Instead of letting the program stop, you can catch the error:
try {
const data = JSON.parse("{bad json");
console.log(data);
} catch (error) {
console.log("Could not read the data.");
console.log(error.name, "-", error.message);
} finally {
console.log("Done.");
}
The output is:
Could not read the data.
SyntaxError - Expected property name or '}' in JSON at position 1 (line 1 column 2)
Done.
- Code in
tryruns normally. If something throws, JavaScript jumps straight tocatch, skipping the rest of thetryblock. catch (error)receives the error object. It hasname,message, andstack.finallyruns every time, error or not. Use it for cleanup, such as hiding a "Loading..." message.
A word of caution: do not use an empty catch that hides errors.
catch {} makes bugs invisible and much harder to find. Catch only what
you can handle, and report or re-throw the rest.
try/catch also works with async/await. In
Asynchronous JavaScript,
a failed await fetch(...) can be caught the same way.
5. Throwing Your Own Errors
You can raise an error yourself with throw. This is how a function says
"I cannot continue with this input":
function setAge(age) {
if (typeof age !== "number" || age < 0) {
throw new Error("Age must be a number, 0 or higher.");
}
return age;
}
Always throw an Error object (new Error("...")), not a string. An
Error object records a stack trace; a string does not.
Write messages that help the reader. Compare:
| Poor message | Better message |
|---|---|
"Invalid" | "Email must contain an @ sign." |
"Error 5" | "Project title is required." |
If you need to tell different problems apart, create your own error type
by extending Error:
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
Now catch can check error instanceof ValidationError and treat user
mistakes differently from real bugs.
6. Project: Validating the Contact Form
Dara's page has a contact form from the HTML chapters. Browsers already
check required and type="email", but you should never trust that
alone, and a script lets you show friendlier messages.
First, the validation logic as a plain function. It throws a
ValidationError for the first problem it finds:
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validateContact({ name, email, message }) {
if (name.trim() === "") {
throw new ValidationError("name", "Please enter your name.");
}
if (!email.includes("@") || !email.includes(".")) {
throw new ValidationError("email", "Please enter a valid email address.");
}
if (message.trim().length < 10) {
throw new ValidationError("message", "Your message must be at least 10 characters.");
}
}
Then connect it to the page. This assumes the form has id="contact-form",
fields named name, email, and message, and a <p id="form-error"> for
messages:
const form = document.querySelector("#contact-form");
const errorBox = document.querySelector("#form-error");
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = {
name: form.elements.name.value,
email: form.elements.email.value,
message: form.elements.message.value,
};
try {
validateContact(data);
errorBox.textContent = "Thank you! Your message looks good.";
form.reset();
} catch (error) {
if (error instanceof ValidationError) {
errorBox.textContent = error.message;
form.elements[error.field].focus();
} else {
throw error;
}
} finally {
console.log("Form checked at", new Date().toLocaleTimeString());
}
});
Notice the else { throw error; }. If the error is not a
ValidationError, it is a real bug, so we let it continue instead of
hiding it. Since this site has no backend, the form does not send
anything yet; sending needs a server, which is outside this series.
7. Debugging with console
The simplest debugging tool is printing values. You have used
console.log; the console has more methods:
| Method | Use it for |
|---|---|
console.log(x) | General output |
console.warn(x) | Something suspicious (yellow) |
console.error(x) | A problem (red, with a stack trace) |
console.table(array) | An array of objects as a table |
console.group("name") / console.groupEnd() | Indent related messages |
console.time("t") / console.timeEnd("t") | Measure how long code takes |
const projects = [
{ title: "Portfolio", tag: "html" },
{ title: "Quiz App", tag: "javascript" },
];
console.table(projects);
console.log("filtering by", "css");
console.log({ tag: "css" });
Tip: wrap a variable in braces, console.log({ tag }). It prints the name
and the value together, so you always know which line produced which
output.
Printing is quick, but it has limits. You must guess where to print, edit the file, and reload. For harder problems, use a debugger.
8. The DevTools Debugger
A debugger pauses your program in the middle so you can look at every variable and move one step at a time.
- Open DevTools (
F12) and choose the Sources panel (Chrome/Edge) or Debugger (Firefox). - Open your script file in the file list.
- Click a line number to set a breakpoint. A marker appears.
- Trigger the code (for example, submit the form).
The page freezes on that line, before it runs. Now you can:
| Control | What it does |
|---|---|
Resume (F8) | Run until the next breakpoint |
Step over (F10) | Run this line, stop at the next one |
Step into (F11) | Go inside the function being called |
Step out (Shift+F11) | Finish this function and return to the caller |
While paused, the Scope pane lists every variable and its current value. Hover over a variable in the code to see it. The Call Stack pane is the same stack trace you saw earlier, now clickable.
You can also write a pause into your code:
function validateContact({ name, email, message }) {
debugger;
// ...
}
When DevTools is open, execution stops at debugger. If DevTools is
closed, nothing happens. Remove these lines before publishing.
A good method for any bug:
- Reproduce it. Find steps that always cause it.
- Narrow it down. Which line first shows a wrong value?
- Form one guess, change one thing, and test again.
- Explain it aloud (to a friend, or even to a rubber duck). Saying it often reveals the mistake.
9. Common Beginner Bugs
| Symptom | Likely cause | Fix |
|---|---|---|
x is not defined | Typo, or variable declared inside another block | Check spelling and scope |
Cannot read properties of null | querySelector found nothing (wrong selector, or script runs before the HTML loads) | Check the selector; add defer to the script tag |
x is not a function | Wrong name, or the property is not a function | Log the value and its type |
"5" + 1 gives "51" | Text and number mixed | Convert with Number("5") |
if (x = 5) is always true | = assigns; === compares | Use === |
0.1 + 0.2 is 0.30000000000000004 | Decimal numbers are stored approximately | Round for display with toFixed(2) |
| Loop never ends | The condition never becomes false | Check the update step; log the counter |
| Changes do not appear | Browser cached the old file | Hard refresh (Ctrl+Shift+R) |
| Array method result unused | sort, map, filter results not stored or returned | Assign the result to a variable |
Most beginner bugs fall into just three groups: a typo, a wrong
assumption about a value (it was null, or a string, not what you
thought), or running at the wrong time (the element did not exist yet).
Printing or pausing to see the real value solves nearly all of them.
10. Try It Yourself
- Read a trace. Save the
broken.jsexample from section 2. Run it, then change the call on line 5 so it passes[3, 4, 5]. Which line did you have to change, and why was it not line 2? - Make each error. Write one line that causes a
SyntaxError,ReferenceError,TypeError, andRangeError. Read each message aloud. - Catch and report. Write
parseAge(text)that usesNumber(text)and throwsnew Error("Age must be a number.")if the result isNaN. Call it insidetry/catchwith"abc"and"25". - Finish the validator. Add the contact form validation to Dara's page. Add one more rule: the name must be at least 2 characters long.
- Use a breakpoint. In your form handler, set a breakpoint on the line
validateContact(data);. Submit with an empty name, then step into the function and watch thenamevariable in the Scope pane. - Fix a hidden bug. Write
const total = "10" + 5;andconsole.log(total). Explain the output, then fix it so it prints15.
Your page can now handle bad input without breaking. In the next chapter, we add types to JavaScript so that many of these mistakes are caught before the code ever runs: TypeScript Basics.
Buy Me a Coffee
Found this tutorial helpful? Consider buying me a coffee to support more free content!