Errors and Debugging

Table of Contents

  1. Errors Are Information
  2. Reading an Error Message
  3. The Four Errors You Will Meet Most
  4. try, catch, and finally
  5. Throwing Your Own Errors
  6. Project: Validating the Contact Form
  7. Debugging with console
  8. The DevTools Debugger
  9. Common Beginner Bugs
  10. 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:

KindWhen it appearsExample
Error (exception)The program stops and the engine reports itUsing a variable that does not exist
Bug (logic error)The program runs but gives the wrong answerA 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:

  1. The location: file broken.js, line 2, with a ^ under the problem.
  2. The type: TypeError.
  3. The message: Cannot read properties of undefined (reading 'reduce'). Read it as a sentence: "I tried to read .reduce from something that was undefined."
  4. 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:

ErrorMeaningTypical cause
SyntaxErrorThe code is not valid JavaScriptMissing bracket, a stray =, bad JSON
ReferenceErrorA name does not existTypo in a variable name, or using it outside its scope
TypeErrorA value cannot be used that wayCalling a non-function, reading a property of null or undefined
RangeErrorA number is outside the allowed rangenew 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 try runs normally. If something throws, JavaScript jumps straight to catch, skipping the rest of the try block.
  • catch (error) receives the error object. It has name, message, and stack.
  • finally runs 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 messageBetter 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:

MethodUse 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.

  1. Open DevTools (F12) and choose the Sources panel (Chrome/Edge) or Debugger (Firefox).
  2. Open your script file in the file list.
  3. Click a line number to set a breakpoint. A marker appears.
  4. Trigger the code (for example, submit the form).

The page freezes on that line, before it runs. Now you can:

ControlWhat 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:

  1. Reproduce it. Find steps that always cause it.
  2. Narrow it down. Which line first shows a wrong value?
  3. Form one guess, change one thing, and test again.
  4. Explain it aloud (to a friend, or even to a rubber duck). Saying it often reveals the mistake.

9. Common Beginner Bugs

SymptomLikely causeFix
x is not definedTypo, or variable declared inside another blockCheck spelling and scope
Cannot read properties of nullquerySelector found nothing (wrong selector, or script runs before the HTML loads)Check the selector; add defer to the script tag
x is not a functionWrong name, or the property is not a functionLog the value and its type
"5" + 1 gives "51"Text and number mixedConvert with Number("5")
if (x = 5) is always true= assigns; === comparesUse ===
0.1 + 0.2 is 0.30000000000000004Decimal numbers are stored approximatelyRound for display with toFixed(2)
Loop never endsThe condition never becomes falseCheck the update step; log the counter
Changes do not appearBrowser cached the old fileHard refresh (Ctrl+Shift+R)
Array method result unusedsort, map, filter results not stored or returnedAssign 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

  1. Read a trace. Save the broken.js example 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?
  2. Make each error. Write one line that causes a SyntaxError, ReferenceError, TypeError, and RangeError. Read each message aloud.
  3. Catch and report. Write parseAge(text) that uses Number(text) and throws new Error("Age must be a number.") if the result is NaN. Call it inside try/catch with "abc" and "25".
  4. 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.
  5. 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 the name variable in the Scope pane.
  6. Fix a hidden bug. Write const total = "10" + 5; and console.log(total). Explain the output, then fix it so it prints 15.

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!