JavaScript TypeErrors and Techniques to Prevent Them
A short guide on best practices, techniques and existing tooling available to prevent TypeErrors in JavaScript or TypeScript.

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
A 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
A 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
A 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
A 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:
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 uncaughtException
event 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
A 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 catch visual regressions in web applications without writing or maintaining UI tests.
Inject the Meticulous snippet onto production or staging and dev environments. This snippet records user sessions by collecting clickstream and network data. When you post a pull request, Meticulous selects a subset of recorded sessions which are relevant and simulates these against the frontend of your application. Meticulous takes screenshots at key points and detects any visual differences. It posts those diffs in a comment for you to inspect in a few seconds. Meticulous automatically updates the baseline images after you merge your PR. This eliminates the setup and maintenance burden of UI testing.
Meticulous isolates the frontend code by mocking out all network calls, using the previously recorded network responses. This means Meticulous never causes side effects and you don’t need a staging environment.
Learn more here.
Authored by Daniel Li