JavaScript Error Types: Complete Guide

JavaScript has seven core error types beyond the base Error. Learn when each occurs, what their names and messages look like, and how to handle them correctly with try...catch.

7 min read

JavaScript has seven core built-in error types. Each one is thrown in specific situations by the engine, and each can be caught and checked with instanceof in a try...catch block. Knowing which error type you are dealing with helps you write better messages, filter errors in a catch block, and understand what went wrong from a stack trace.

All error types inherit from Error, so you can check the type of any caught error through its name property or with the instanceof operator.

Error typeTrigger condition
ErrorGeneric base error
TypeErrorWrong type for an operation
ReferenceErrorUsing an undeclared variable
RangeErrorValue outside allowed range
SyntaxErrorInvalid syntax in parsed code
URIErrorMalformed URI parameter
AggregateErrorMultiple errors wrapped into one

TypeError

TypeError is the most common runtime error. It means you tried to do something with a value that is not valid for its type: calling a non-function, accessing a property on null or undefined, or using new on something that is not a constructor.

javascriptjavascript
const x = null;
try {
  x.property;
} catch (e) {
  console.log(e.name);                  // "TypeError"
  console.log(e instanceof TypeError);  // true
}

Reading a property off null has no meaningful result, so the engine throws instead of returning undefined. Calling a non-function value the same way you would call a function produces the same error type:

javascriptjavascript
try {
  "hello"();
} catch (e) {
  console.log(e.message); // "\"hello\" is not a function"
}

In your error handling, TypeError often means a value is unexpectedly null or undefined. This is one of the most common bug categories in production JavaScript, and it is usually worth logging the variable name along with the error.

ReferenceError

ReferenceError is thrown when you try to use a variable that has not been declared anywhere in scope. The engine looks up the name in the current scope chain, fails to find it, and throws.

javascriptjavascript
try {
  console.log(nonExistentVariable);
} catch (e) {
  console.log(e.name);    // "ReferenceError"
  console.log(e.message); // "nonExistentVariable is not defined"
}

ReferenceError also happens when you access a let or const variable before its declaration runs, a period called the temporal dead zone:

javascriptjavascript
try {
  console.log(city); // ReferenceError, not yet initialized
  let city = "Austin";
} catch (e) {
  console.log(e instanceof ReferenceError); // true
}

The key difference from TypeError is that ReferenceError means the variable was never declared, while TypeError means the variable exists but was used the wrong way. Writing let n; n.length throws a TypeError, not a ReferenceError, because n was declared, just not given a usable value.

RangeError

RangeError fires when a numeric value falls outside the allowed range for an operation. Common cases include invalid array lengths and numbers too large for toFixed or toPrecision.

javascriptjavascript
try {
  new Array(-1); // Negative array length
} catch (e) {
  console.log(e.message); // "Invalid array length"
}
 
try {
  (1.23).toFixed(101); // toFixed only accepts 0 to 100
} catch (e) {
  console.log(e instanceof RangeError); // true
}

RangeError also fires when the call stack overflows from unbounded recursion, which is a different kind of range problem: the recursion depth itself exceeds what the engine allows.

javascriptjavascript
function recurse() {
  recurse();
}
 
try {
  recurse();
} catch (e) {
  console.log(e.message); // "Maximum call stack size exceeded"
}

SyntaxError

The parser throws SyntaxError when code contains invalid syntax. A syntax error in the static script itself cannot be caught at runtime, because the parser rejects the whole script before any code executes. SyntaxError can only be caught when it comes from dynamic code evaluation, such as JSON.parse or eval:

javascriptjavascript
try {
  JSON.parse("{ bad json }");
} catch (e) {
  console.log(e instanceof SyntaxError); // true
}

eval and the Function constructor throw the same way when the string of code they receive fails to parse as valid JavaScript, since both compile their argument at the moment they run:

javascriptjavascript
try {
  eval("1 + 2 +");
} catch (e) {
  console.log(e instanceof SyntaxError); // true
}

SyntaxError at runtime is almost always from bad JSON. Always wrap JSON.parse in try...catch whenever the input might be malformed, such as data coming from a network response or user-supplied text.

URIError

URIError is thrown when encodeURI, encodeURIComponent, decodeURI, or decodeURIComponent receive a malformed URI:

javascriptjavascript
try {
  decodeURIComponent("%"); // Incomplete percent encoding
} catch (e) {
  console.log(e instanceof URIError); // true
}

URIError is rare in practice, because most URL parsing in modern JavaScript uses the URL constructor, which throws a TypeError for an invalid URL instead. You will only encounter URIError if you directly call the encodeURI or decodeURI family of functions.

AggregateError

Introduced in ES2021, AggregateError wraps multiple errors into a single error object. Its errors property holds an array of every individual error that contributed to it.

javascriptjavascript
try {
  await Promise.any([
    Promise.reject(new Error("network down")),
    Promise.reject(new Error("timeout")),
  ]);
} catch (e) {
  console.log(e instanceof AggregateError); // true
  console.log(e.errors.length);             // 2
}

Promise.any rejects with an AggregateError only when every promise in the iterable rejects. You can also construct one yourself to report several validation failures at once instead of stopping at the first problem found:

javascriptjavascript
const failures = [
  new Error("File A failed to upload"),
  new Error("File B is too large"),
];
throw new AggregateError(failures, "Upload completed with errors");

The catch binding and destructuring

You can destructure the caught error directly in the catch clause to pull out just the fields you need:

javascriptjavascript
try {
  throw new TypeError("Expected a string, got number");
} catch ({ name, message }) {
  console.log(`[${name}] ${message}`); // "[TypeError] Expected a string, got number"
}

This works because catch accepts any binding pattern, the same way a function parameter does, so destructuring name and message skips having to write error.name and error.message separately.

Handling multiple error types

A well-written catch block checks the error type before deciding what to do, instead of treating every failure the same way:

javascriptjavascript
async function loadData(url) {
  try {
    const response = await fetch(url);
    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      return { error: "Network error. Check your connection." };
    }
    throw error;
  }
}

A TypeError here means fetch itself failed, usually from a dropped connection, so the function returns a friendly message instead of crashing. Any other error type, such as a SyntaxError from a response body that failed to parse, gets rethrown so it stays visible instead of being silently treated as a network problem.

Error type decision flow in catch block

Each branch in the diagram handles one known error category. Unknown errors fall through to the final branch, which logs and rethrows so they stay visible for debugging instead of being silently swallowed.

Creating your own error types

When none of the built-in types capture the meaning of your error, extend Error to create a named type of your own:

javascriptjavascript
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

The field property here is something none of the seven built-in types can carry, which is exactly the case where a custom error earns its keep:

javascriptjavascript
try {
  throw new ValidationError("Email format is invalid", "email");
} catch (error) {
  if (error instanceof ValidationError) {
    highlightField(error.field);
  }
}
``` For a full guide on custom errors, see [creating custom errors in JavaScript](/javascript/creating-custom-errors-in-js-complete-tutorial). For the fundamentals of catching errors, see the [try...catch guide](/javascript/js-try-catch-tutorial-advanced-error-handling).
Rune AI

Rune AI

Key Insights

  • JavaScript has 7 core error types: Error, TypeError, ReferenceError, RangeError, SyntaxError, URIError, and AggregateError.
  • TypeError is the most common runtime error, triggered by operations on values of the wrong type.
  • ReferenceError means a variable was used without being declared.
  • SyntaxError at runtime only comes from dynamic code like JSON.parse(), not from static script parsing.
  • AggregateError groups multiple errors, useful for Promise.any() and batch operations.
  • Use instanceof in catch blocks to handle each error type appropriately.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between TypeError and ReferenceError?

A ReferenceError means you tried to use a variable that does not exist. A TypeError means the variable exists but you tried to use it in an invalid way, like calling a number as a function or reading a property on null.

Can I catch SyntaxError at runtime?

Yes, but only syntax errors in dynamically evaluated code like JSON.parse() or eval(). Syntax errors in static script code are caught by the parser before the code runs and cannot be caught by try...catch in the same script.

What is AggregateError used for?

AggregateError wraps multiple errors into one. It is most commonly used with Promise.any(), which rejects with an AggregateError containing all the individual rejection reasons when every promise fails.

Conclusion

JavaScript has a structured error system, not just one generic Error type. Understanding which error type your code might throw lets you write precise catch blocks with instanceof checks, display more helpful messages to users, and avoid silently swallowing errors you should not handle. When none of the built-in types fit your use case, extend Error and create your own.