JavaScript TypeErrors and Techniques to Prevent Them

updated on 16 August 2022

When a JavaScript engine encounters a problem it can’t resolve, it will create an object that extends the Error interface and throws it. If the error happens in the browser, you’ll see a red error printed in the Developer Tools Console tab, and the page may stop responding; if the error happens in a Node.js program, the program will crash.

When writing resilient applications, it’s important to learn what the different types of errors are and how to handle them appropriately.

There are various errors that a JavaScript engine may encounter, including a SyntaxError, a ReferenceError, and a TypeError. This article will specifically look at the TypeError, which is thrown when you try to access properties that are invalid for that data type.

In this article, you’ll learn some of the reasons a TypeError occurs, techniques to prevent them, and how to handle a TypeError when it does occur.

What Causes TypeErrors

TypeError occurs when an operation cannot be performed because the value is not of the expected type. Here are a few examples:

Calling a Non-function

TypeError can occur when you are invoking a value as a function but that value is not actually a function. In the following code snippet, the code is calling obj as a function, and so the JavaScript engine throws a TypeError with the error message obj is not a function:

const obj = {}
obj()

Similarly, the following snippet would print a foo.forEach is not a function error message because forEach is undefined and the code is calling it as if it were a function:

const foo = 'hello'
foo.forEach(console.log)

Accessing Properties of Undefined or Null

TypeError can also occur when you’re trying to access a property of a value but that value is nullish (ie undefined or null).

In the following code snippet, Object is a built-in class; however, unknownProp is not a property of Object, which means Object.unknownProp is undefined. The code then tries to access the method property of an undefined value, so it will throw a TypeError with the error message Cannot read properties of undefined (reading 'method'):

Object.unknownProp.method()

Having Manually Thrown TypeErrors

The TypeError is not only thrown by the JavaScript engine. Libraries and program code can also manually throw a TypeError when they encounter a type-related problem.

For example, the app.use() function in the Express.js library expects to be passed a middleware function or an array of middleware functions. There is a check in the definition of app.use that throws a TypeError when it is not passed a middleware function (or an array of them):

if (fns.length === 0) {
  throw new TypeError('app.use() requires a middleware function')
}

Other Causes

X is not a function and Cannot read properties of undefined errors tend to be the most commonly encountered TypeError. Other less common TypeError thrown by JavaScript engines include the following:

Iterating Over Non-iterables

TypeError can be thrown when you use language-native constructs (eg for...of) to iterate a value but that value is not iterable. The following code will output the error message TypeError: 42 is not iterable:

for (const prop of 42) {}

Using the New Operator on a Non-object Type

The new operator allows a developer to create a new instance of a class or function constructor. However, if you use the new operator on a value that cannot be called as a constructor function, a TypeError will be produced.

The following code will output the error message Foo is not a constructor:

const Foo = 'string'
const bar = new Foo()

JSON-Stringifying an Object with a Circular Reference

Built-in objects and methods can also throw a TypeError. For example, the JSON.stringify method will throw a TypeError when a circular reference is found in the object being stringified or when the object contains a BigInt value:

const foo = {};
foo.bar = foo;
JSON.stringify(foo);

The previous code snippet will output the following error message:

 TypeError: Converting circular structure to JSON
  --> starting at object with constructor 'Object'
  --- property 'bar' closes the circle

How to Prevent TypeErrors

Now that you know some common causes for a TypeError to occur, we can explore some ways to prevent it from being thrown in the first place.

Adopt Static Type Checking

Many TypeError instances can be prevented by using a static type checker, like Flow, or by using TypeScript, a superset of JavaScript that supports static types.

With both Flow and TypeScript, you first need to annotate your code with type information about each value. For example, in the following code snippet, : number [] tells the type checker that this function expects a single parameter of the numeric array type; the : number after the parameter list tells the type checker that this function is expected to return a single value of type number:

function max(numbers: number[]): number {
  return numbers.reduce((h, c) => Math.max(h, c), Number.MIN_SAFE_INTEGER)
}

Not every value needs to be explicitly annotated. For example, in the following snippet, the TypeScript compiler is smart enough to know that foo is of type string, even though you haven’t explicitly annotated it with foo: string:

const foo = 'hello'

Once your code has been annotated, you can set up your editor or integrated development environment (IDE) to continuously check the code for TypeError instances. If one is found, the error will be highlighted in the editor/IDE, typically by underlining the offending code with a red squiggly line. You can hover over the line to reveal a pop-up that explains what the problem is.

For example, if you try to pass in the numbers four times as multiple arguments instead of as a single array of numbers to the max function defined earlier, the type checker will notice that this contradicts the type annotation and will output the error Expected 1 arguments, but got 4, which will be displayed on the IDE:

IDE showing a TypeError-gv68r

Apart from running the type checker locally with your editor/IDE, you should also run it as part of the CI/CD build process. This will ensure that the checks are passing based on the committed version of the code and not the local version, which may have uncommitted changes that make the checks pass locally.

Static Type Checking Best Practices

Static type checking can drastically reduce the number of TypeError instances if you annotate your code correctly. If, however, the provided annotation is incorrect or too broad (eg using the any keyword), then the static type checker is prevented from working correctly and a TypeError may creep in.

For example, in TypeScript, you can use type assertion to tell the static type checker that a value conforms to a certain interface.

In the following snippet, you are asserting that db.get will return an object that conforms to the User interface:

const user = db.get(userId) as User
res.send(user.preferences.theme)

However, if this assumption is incorrect and the user.preferences is nullish, the code will produce a TypeError, even though you’re using a static type checker.

Another common pitfall to avoid when writing in TypeScript is the overuse of the any type, which is a type that allows the value to be anything (eg strings, booleans, and numbers). This type is so broad that it essentially opts the value out of being type-checked.

The following TypeScript code snippet prevents the TypeScript compiler from checking the user variable. This means you’d have no idea whether user.preferences.theme will be an error until you run it:

const user = db.get(userId) as any
res.send(user.preferences.theme)

Generally speaking, you should avoid using the any type unless your function is actually designed to handle objects of any shape. For example, if you’re writing a utility function that adds a method to an arbitrary value, then any may be the appropriate type to use.

In summary, when using static type checking, make sure that any type annotations and assertions are correct and make sure that type definitions are not too broad; otherwise, the static type checker won’t be able to do its job properly.

Add Runtime Checks

Using static type checkers alone will not eliminate all TypeError instances. This is because static type checking only works for values with a type that is known at compile time. In many scenarios, the precise type and shape of a variable are only known at runtime. For example, the shape of a parsed HTTP request body depends on user input and, thus, is only known at runtime.

Take the following Express.js snippet:

app.patch('/theme', (req, res) => {
  db.update({ color: req.body.company.appearance.color });
})

The code assumes that the request body can be parsed as an object with the nested property company.appearance.color, but if the actual request body is a simple string, then the previous code will produce a TypeError with the message Cannot read properties of undefined.

To prevent the TypeError, you can explicitly write some code to check these runtime values using conditional blocks. For example, in the following snippet, we are responding with a 400 Bad Request status code if the request does not provide a request body with the right shape:

app.patch('/theme', (req, res) => {
  if (!(req.body && req.body.company && req.body.company.appearance)) {
    return res.status(400).end()
  }
  db.update({ color: req.body.company.appearance.color });
})

Apart from preventing TypeError instances, it’s good practice to validate user input and data obtained from third-party APIs.

But writing a bunch of if statements for every value you want to use may make the code verbose and less maintainable. Instead, you can use tools like JSON Schema and OpenAPI to define the expected shape of your values and rely on tooling to validate the actual values against the expected values.

Use the Optional Chaining Operator

You can also use the optional chaining operator (?.), which will return undefined if any part of the chain is nullish. In the following code snippet, if any of req.body, req.body.company, or req.body.company.appearance are null or undefined, then the expression req.body?.company?.appearance?.color would be undefined:

app.patch('/theme', (req, res) => {
  db.update({ color: req.body?.company?.appearance?.color });
})

How to Handle Unforeseen TypeErrors

Adopting type checking and following best practices can help prevent TypeError instances, but some may still sneak through. To make your application resilient to errors, you also need to handle unforeseen errors.

Try-Catch

JavaScript provides the try...catch construct; any errors (not just TypeError) thrown within the try block are caught and handled in the catch block.

Earlier, you saw that calling Object.unknownProp.method() would throw a Cannot read properties of undefined (reading 'method') error. But when you wrap that call in a try block, the TypeError that is thrown will be passed to the catch block and handled without interrupting the program’s execution:

try {
  Object.unknownProp.method();
} catch (e) {
  console.log(e.name)
  console.log(e.message)
}
// ...continue execution

Every error thrown is an extension of the Error interface, which always has a name and a message property. You can use the name and/or message to decide how to handle an error.

Note that try...catch blocks can be nested. Errors will be handled by the nearest block. If you want the error to bubble up, you can rethrow the error:

try {
  produceError();
} catch (e) {
  if (e.name === 'TypeError') {
    console.log(e.name)
    console.log(e.message)
  } else {
    throw e; // re-throw the error
  }
}

You could wrap the entirety of your application’s code in a try...catch construct, which would catch any errors that were not caught and handled. However, this is a bad practice and you should not do this, as it is hard to write a good error-handling function that can precisely cater to a wide range of errors. Instead, you should catch errors as close to the source of the error as possible. You can read more about how to appropriately use try-catch in this article.

Catch-All

In your browser, there’s a global window.onError method, which is called when any thrown errors are not handled. You can assign a handler to window.onError that acts as a global, catch-all error handler for all errors not caught by a try...catch block.

In Node.js, there’s no window object and, thus, no window.onError handler. Instead, you can listen for the uncaughtException event and handle the error:

process.on('uncaughtException', error => {
  console.error('Uncaught exception:', error);
  process.exit(1);
});

Note that the browser’s window.onError method and Node.js’s uncaughtException
 event handler should only be used as a last resort. Whenever possible, errors should be caught and handled close to where the error occurs. This provides the programmer with the most information about where the error occurred, and this prevents the programmer from writing a complex error handler function.

The Node.js documentation recommends that the uncaughtExceptionevent handler should still exit the process after cleaning up any allocated resources. The program should assume that the unhandled exceptions mean the program is in an undefined state and that it is not safe to resume execution once an error reaches the uncaughtException event handler.

How to Fix TypeErrors

A TypeError can be fixed by comparing the actual value with the type definition and identifying any disparities. Once identified, either the type definition needs to be revised or the value needs to be processed to conform to the type.

Also, note that the thrown error may not be the root cause of the problem but only a symptom. For example, the line const username = res.body.user.name may throw a Cannot read properties of undefined error because res.body.user is undefined. While you should definitely be handling the case where res.body.user is undefined, the root of the issue may be that you are not catering to cases when the web server is unreachable and the response object is undefined:

if (!(res && res.body)) {
  // implement retry logic
}
const username = res.body.username

How Meticulous Can Help

One of the best ways to identify a TypeError is by writing and running tests so that the errors surface during testing. However, writing good and comprehensive tests is hard and time-consuming.

Meticulous is a testing tool that allows you to record workflows and simulate these on new frontend code. The record feature can help save you many hours of test-writing, and the replay feature can help surface any new uncaught errors (like the TypeError).

If you want to prevent the TypeError from popping up on your frontend code, try Meticulous or check out the docs.

Conclusion

TypeError occurs when a value is not of the expected type. You can prevent a TypeError from occurring by using a static type checker, like Flow, or by writing your code in TypeScript. Make sure the type annotations you write are accurate and not too broad.

You should assume that a TypeError will inevitably sneak through, so use try...catch blocks to catch and handle them. Lastly, implement a test pipeline that identifies the TypeError before it goes to production.

Meticulous

Meticulous is a tool for software engineers to easily create end-to-end tests. Use our open source CLI to open an instrumented browser which records your actions and translates them into a test. Meticulous makes it easy to integrate these tests into your CI.

Meticulous has an option to automatically mock out all network calls when simulating a recorded sequence of actions. If you use this option, you do not need a backend environment to use Meticulous and Meticulous tests never cause side effects (like affecting analytics) or hit your backend.

Try out the open-source CLI and create your first test in 60 seconds using our docs or watch the demo.

Authored by Daniel Li

Read more