An Introduction to Async Stack Traces in Node.js
Tags: node, express, javascript, webdev
Published: 2023-05-24 11:19:30

Node.js 12.x onwards introduced async stack traces. Async stack traces allow developers to view the call stack of asynchronous code, making it easier to trace and debug code issues.

In this post, we’ll see how async stack traces work, how you can use them to debug code, and how to track them using AppSignal. Ready?

Let's get going!

## How Async Stack Traces Work in Node.js

In JavaScript, asynchronous code is executed through callbacks, promises, and async/await. When an error occurs in asynchronous code, it can be difficult to trace its origin because the call stack is not captured at the point of the error. Async stack traces capture the call stack when the asynchronous code is scheduled rather than when it is executed, allowing developers to see an error's full context.

Node.js uses an event-driven, non-blocking I/O model for handling asynchronous code execution. An event loop constantly checks for new tasks, executes them one by one, and registers callback functions for tasks that do not block the execution. This allows Node.js to handle multiple tasks simultaneously, improving performance and scalability.

Async stack traces capture the call stack when an async operation begins using "long stack traces". When an async operation starts, Node.js creates a new execution context that includes information about the current call stack and the location where the async operation was initiated. This execution context is then passed along with the async operation as it is scheduled and executed.

When an error occurs, Node.js can reference the execution context to determine the entire call stack at the point where the async operation began. It enriches the stack property of `Error` instances with async stack frames, i.e., await locations in the code. These async frames are marked with `async` in the stack string. You can see the full context of an error, including the function calls and variables that led to the error.

## Node.js Async Stack Traces: A Code Example

Consider the following code. An async function `init()` calls `funcOne()`, which then calls `funcTwo()`, which then calls and throws `funcThree()`. Async stack traces will print a stack trace with `init()`, `funcOne()`, `funcTwo()`, and `funcThree()`.

```javascript
const funcThree = async () => {
  await Promise.resolve();
  throw new Error("Oops");
};

const funcTwo = async () => {
  await Promise.resolve();
  await funcThree();
};

const funcOne = async () => {
  await new Promise((resolve) => setTimeout(resolve, 10));
  await funcTwo();
};

const init = async () => {
  await new Promise((resolve) => setTimeout(resolve, 10));
  await funcOne();
};

init().then(
  () => console.log("success"),
  (error) => console.error(error.stack)
);
```

This results in an output like so:

```shell
node ./asyncStackTraces.js

Error: Oops
    at funcThree (/Users/kennethjimmy/Desktop/asyncStackTraces.js:3:9)
    at async funcTwo (/Users/kennethjimmy/Desktop/asyncStackTraces.js:8:3)
    at async funcOne (/Users/kennethjimmy/Desktop/asyncStackTraces.js:13:3)
    at async init (/Users/kennethjimmy/Desktop/asyncStackTraces.js:18:3)
```

If we run the same code on a version of Node.js older than 12, the output looks like this:

```shell
node ./asyncStackTraces.js

Error: Oops
    at funcThree (/Users/kennethjimmy/Desktop/asyncStackTraces.js:3:9)
    at <anonymous>
```

In this case, `at <anonymous>` isn’t very helpful in revealing the full context of the error.

However, this works differently with async stack traces. When an error occurs in a callback function, the call stack is captured at the point when a callback is scheduled (the `init()` function in this case), rather than when it is executed.

## Set Up AppSignal for Your Express.js App

[AppSignal](https://www.appsignal.com/nodejs) can assist you in identifying and troubleshooting problems with your app, including issues related to slow performance, errors, crashes, and downtime. You can resolve these issues quickly before they negatively impact your users.

In this tutorial, we will walk you through how to [integrate AppSignal into your Express.js app](https://www.appsignal.com/nodejs/express-js-performance-monitoring).

_Note: [You can follow along with the source code for the following tutorial here](https://github.com/Glitzyken/async-stack-traces-appSignal "source code")._

Firstly, [sign up for an account on appsignal.com](https://appsignal.com/users/sign_up) and create a new app by following [these guides](https://docs.appsignal.com/guides).

Then start a new Node.js project:

```shell
npm init -y
```

Install AppSignal for Node.js:

```shell
npm install --save @appsignal/nodejs
```

In your repo’s root directory, create an `appsignal.cjs` file and paste the following code to require and configure AppSignal:

```javascript
require("dotenv").config();
const { Appsignal } = require("@appsignal/nodejs");

new Appsignal({
  active: true,
  name: process.env.APPSIGNAL_APP_NAME,
  pushApiKey: process.env.APPSIGNAL_PUSH_API_KEY, // Note: renamed from `apiKey` in version 2.2.5
});
```

_Note: An application's Push API key is given during setup and can be found in the App settings > Push & deploy tab._

In your `package.json` file, edit the `start` script to load AppSignal's configuration before any other library using the `--require` flag, like so:

```json
{
  "scripts": {
    "start": "node --require ‘./appsignal.cjs’ app.mjs"
  }
}
```

Integrating AppSignal into your Express app is very easy. You just need to require the `expressErrorHandler` module and then use it in your app with `app.use`, as shown below:

```javascript
import { expressErrorHandler } from "@appsignal/nodejs";

const app = express();

// add this after all routes, but before any other error handlers
app.use(expressErrorHandler());
```

The `expressErrorHandler` module consists of middleware designed to forward any errors in your Express application to AppSignal.

## Tracking Async Callback Errors with AppSignal

Let's create an error inside an asynchronous callback to see how AppSignal handles it:

```javascript
const funcThree = async () => {
  await Promise.resolve();
  throw new Error("Oops");
};

const funcTwo = async () => {
  await Promise.resolve();
  await funcThree();
};

const funcOne = async () => {
  await new Promise((resolve) => setTimeout(resolve, 10));
  await funcTwo();
};

app.get(
  "/trigger_error",
  catchAsync(async (req, res) => {
    await new Promise((resolve) => setTimeout(resolve, 10));
    await funcOne();

    res.status(200).json({
      statusbar: "success",
      data: {
        mentors,
      },
    });
  })
);
```

When accessing `GET /trigger_error`, an error is triggered. The server will return the error along with an asynchronous stack trace, as shown below:

```shell
{
  "status": "error",
  "error": {
      "statusCode": 500,
      "status": "error"
  },
  "message": "Oops",
  "stack": "Error: Oops\n    at funcThree (file:///Users/kennethjimmy/Desktop/app.mjs:29:9)\n    at async funcTwo (file:///Users/kennethjimmy/Desktop/app.mjs:34:3)\n    at async funcOne (file:///Users/kennethjimmy/Desktop/app.mjs:39:3)\n    at async file:///Users/kennethjimmy/Desktop/app.mjs:55:5"
}
```

After the error is thrown, AppSignal captures and reports the exception. To view your application's errors and stack traces, navigate to the AppSignal Dashboard, select "Errors", and click on "Issue list."

![Error list](https://blog.appsignal.com/images/blog/2023-05/error-list.png)

Click on a particular error to see its details, including the error message and stack trace.

![Error message](https://blog.appsignal.com/images/blog/2023-05/error-message.png)

## Increasing Stack Trace Size in Node.js

By default, stack trace size is limited, making it difficult to pinpoint the exact source of an error. In development, we want as much context as we can get. Fortunately, it's possible to increase stack trace size in Node.js.

To do so, you can use the `stackTraceLimit` property of the `Error` object/class. By default, this property is set to 10, so only the last 10 calls will be included in the stack trace. If the value is set to 0, this stops the collection of stack traces. However, any positive integer can be assigned as the maximum limit for collecting frames. Alternatively, setting the property to `Infinity` will collect all the frames.

For example, to set the stack trace limit to `Infinity` in an Express.js application in development mode, you can use the following code:

```shell
// Set the stack trace limit to Infinity in development mode
if (process.env.NODE_ENV !== 'production') {
  Error.stackTraceLimit = Infinity
}

// Initialize Express.js app
const express = require('express');
const app = express();

// Define routes
// ...

// Start the server
const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server started on port ${port}`);
});
```

By increasing stack trace size, you can get more information about the execution of your code and better understand the context in which an error occurs. However, it's important to consider the following:

- **Performance impact:** Increasing stack trace size can negatively impact performance, particularly in cases where a large number of errors occur, or the stack trace size is set to an excessively high value.
- **Memory usage:** A larger stack trace size means more memory usage, which can be a concern if your application is already memory-intensive.
- **Security risks:** Longer stack traces can potentially expose sensitive information, such as credentials or source code, if not properly handled.
- **Complexity:** It can be more difficult to navigate and analyze a larger stack trace, particularly for complex applications.

With that in mind, you should use a reasonable limit that provides the necessary information without causing performance issues.

## Wrapping Up

In this post, we've seen how async stack traces in Node.js can help you debug and trace errors in your asynchronous code by capturing call stacks.

We also explored how integrating AppSignal into your Node.js application makes it a lot easier to detect and resolve async callback errors in real time.

Finally, we discussed how expanding stack trace size in Node.js can offer more insights into the execution of your code and help you understand an error's context. However, it's important to use this feature judiciously, taking into account its possible impact on performance, memory usage, security, and navigability.

Happy coding!

**P.S. If you liked this post, [subscribe to our JavaScript Sorcery list](https://blog.appsignal.com/javascript-sorcery) for a monthly deep dive into more magical JavaScript tips and tricks.**

**P.P.S. If you need an APM for your Node.js app, go and [check out the AppSignal APM for Node.js](https://www.appsignal.com/nodejs).**