JavaScript Stack Trace: Understanding It and Using It to Debug

We’ve all been there. You’ve set up a new project, everything is going smoothly, and boom, it hits. You’re staring at a big red error in your console. After trying everything you can think of, you just can’t seem to resolve it. In desperation, you resort to posting the error up on the internet. Help!

Sure enough, an anonymous hero, “Linux412,” rides in and informs you, “Looks like you’ve got a naming error on your function.” Aha—that fixed it, but how did they know?! The secret to the anonymous developer’s magic lies in their ability to read and make sense of application error reports.

If we learn how to read these errors, we’ll be able to solve nearly all of our coding problems. In JavaScript, the error report is called a stack trace. We already covered the stack trace in PHP and Java in earlier articles. But today, it’s the JavaScript stack trace that’s under discussion.

By the end of this article, you should understand what the JavaScript stack trace is and why naming functions and error messages are important for debugging with stack traces.

JavaScript fly swatter signifying javascript stack trace

What Is a Stack Trace?

As always, let’s start by defining a stack trace.

A stack trace is a list of the functions, in order, that lead to a given point in a software program.

A stack trace is essentially a breadcrumb trail for your software. You can easily see the stack trace in JavaScript by adding the following into your code:

console.trace();

And you’ll get an outputted stack trace.

But what does a stack trace actually look like? Let’s cover that now.

The Anatomy of a Stack Trace

To understand the stack trace better, let’s take an example.

function controller(){
    database();
}

function database(){
    query();
}

function query () {
    throw new Error();
}

Here we see a series of functions that call each other. The last function will error, causing the program to halt and emit a stack trace.

When the last function errors, the following stack trace is emitted:

Uncaught Error
    at query (:10:11)
    at database (:6:5)
    at controller (:2:5)

And you can see from the above error, query was called by database, which in turn was called by controller. You can see then that the stack trace shows which functions called each other. Importantly, we can also see the line number and file that the function exists on, and the topmost line is the error itself.

The stack trace helps us to know the steps that lead up to our error. And the stack trace isn’t only useful for humans. If you’re using error tooling that tracks and stores errors, it can use these stack traces to count how often an error occurs.

But wait—before you take your newfound knowledge of stack traces and start using the console.trace method as a debugging tool, there are a few other things we should consider. In JavaScript, the stack trace may not always appear as you think. What do I mean? Let me explain…

Function Naming, and Why It’s Important

One of the key components in the above stack trace is that it emits not only an error message and a set of line numbers, but the function names. However, in JavaScript, not every function has a name. Some functions can be anonymous.

These anonymous functions pose a threat to our debugging. If our stack traces include anonymous functions, we’ll greatly reduce the value of the stack trace because it’s much harder to see the steps that lead to an error. This will make our whole debugging process so much more painful.

Take, for instance, the following example:

const database = Promise.resolve();
database
    .then(function () {
        throw new Error();
    });

When we run the above code, we see the following stack trace:

Uncaught (in promise) Error
at <anonymous>

As you can see, the error is thrown, but the parent function that led to the error is anonymous. Not only that, but the error itself is quite cryptic. It tells us that an error happened, but no indication is made as to why the error occurred.

The stack trace contains a reference to an unnamed function because in the previous example the error that was thrown was in fact anonymous. Go ahead and take a look. We didn’t tell JavaScript the name of the function, so it can’t report it, which isn’t very useful.

So how can we resolve the problem and go back to understanding the history of each of our called functions? By ensuring that we apply a name property to all of our functions.

Let’s modify the above example with a name on our function.

const database = Promise.resolve(); 
database
    .then(function resolver () { 
        throw new Error(); 
    });

Which now results in the following stack trace:

Uncaught (in promise) Error
    at resolver (<anonymous>:1:81)

Aha! Much better.

Always Name Your Functions

Generally speaking, it’s wise to always name your functions, no matter the situation. You can name a function by adding the name between the function keyword and your argument brackets. But don’t be fooled—an anonymous function assigned to a variable is still anonymous.

For instance, take the following:

const named = function(){}

Here, it appears that the function is named, but it isn’t. Because there’s no name between the function keyword and the round brackets, the function has no name and will appear as anonymous in our stack trace. Some JavaScript engines can infer our name in some scenarios, but it’s not an officially supported feature, so don’t be fooled. Always name your functions.

Give Names to Your JavaScript Errors

The keen-eyed among you will have noticed that in all the examples shown so far, the error messages themselves have been pretty useless. For instance, “Uncaught (in promise) Error” gives us very little context or understanding to fix the error.

But there is hope! We can actually fix these error messages. Let me show you how.

In order for stack traces to be their most effective, we should anticipate as many error states as possible and throw descriptive errors. Say, for instance, that JavaScript encountered a missing piece of data—let’s say a username not in a database. We wouldn’t want our code to continue running, because it would try to perform actions on the missing data and would eventually throw some unhelpful message like:

Uncaught ReferenceError: user is not defined

Seem familiar? It will be to most developers. Because JavaScript is quite flexible, running into these cryptic error messages is all too common.

So, how do we fix it? By throwing errors with descriptive error messages.

Let’s go back to our first example to look at throwing named errors in action. If we take our original example, rather than throwing a generic error, let’s go ahead and add an error message to our error object constructor, as follows:

function controller(){ database(); }

function database(){ query(); }

function query () { throw new Error('Error occurred in query, please try X'); }

Which will result in the following stack trace:

Uncaught Error: Error occurred in query, please try X
at query (<anonymous>:5:27)
at database (<anonymous>:3:22)
at controller (<anonymous>:1:24)

Now that’s a lot more readable!

Notice that we even modified our error message to include a suggestion for a fix.

When you’re writing the code at that point in time, you’ll have a good understanding of all the different ways that your function can fail. So, if you can, explicitly throw errors with meaningful messages to help out your future self when you’re debugging your code.

And That’s All on the Stack Trace

That concludes our look at JavaScript stack traces. I hope it helps you feel more confident when you see stack traces and that you understand how they’re created, how to read them, and ultimately how to use them confidently to debug your application.

Remember, always name functions, and don’t be fooled by assigning them to variables, which are still unnamed. And also, try to predict failure points in your application and throw reasonable error messages that will make your life easier later on.

No more will you need to mindlessly paste errors onto the internet. Instead, you can sift through them yourself like an investigator solving a mystery. Debugging applications in a much more logical and straightforward manner should save you plenty of headaches in the long run, and make writing code a little bit more fun.

Good luck!