Skip to content

Instantly share code, notes, and snippets.

@ugultopu
Created December 20, 2020 17:18
Show Gist options
  • Save ugultopu/3302d0e717fd8b09dc2354329066f22a to your computer and use it in GitHub Desktop.
Save ugultopu/3302d0e717fd8b09dc2354329066f22a to your computer and use it in GitHub Desktop.
Information about the nature and formatting of the stack traces in JavaScript, using Node.js for the examples

When you see a stack trace on your terminal, it is not some special, magical thing. It is simply nothing but a string representation of an Error instance. That is, a stack trace is nothing but the output of the toString method of that Error instance.

When you create an Error instance, an object is created with the following properties which has the following attributes. Put the following in a file named script.js and run it with node script.js:

const e = Error('Some optional message');
console.log(getAllProperties(e));

// This function is from https://gist.github.com/ugultopu/15abb1790b3d08384a372cdef3ab5b81
function getAllProperties(object) {
  // Same as:
  // if (object === null || object === undefined) return;
  if (object == null) return;
  const properties = {...Object.getOwnPropertyDescriptors(object)};
  let currentObject = properties;
  while (object = Object.getPrototypeOf(object)) {
    currentObject.PROTO = Object.getOwnPropertyDescriptors(object);
    currentObject = currentObject.PROTO;
  }
  return properties;
}

Output:

{
  stack: {
    value: 'Error: Some optional message\n' +
      '    at Object.<anonymous> (/path/to/the/script.js:1:11)\n' +
      '    at Module._compile (internal/modules/cjs/loader.js:1063:30)\n' +
      '    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)\n' +
      '    at Module.load (internal/modules/cjs/loader.js:928:32)\n' +
      '    at Function.Module._load (internal/modules/cjs/loader.js:769:14)\n' +
      '    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)\n' +
      '    at internal/main/run_main_module.js:17:47',
    writable: true,
    enumerable: false,
    configurable: true
  },
  message: {
    value: 'Some optional message',
    writable: true,
    enumerable: false,
    configurable: true
  },
  PROTO: {
    constructor: {
      value: [Function],
      writable: true,
      enumerable: false,
      configurable: true
    },
    name: {
      value: 'Error',
      writable: true,
      enumerable: false,
      configurable: true
    },
    message: {
      value: '',
      writable: true,
      enumerable: false,
      configurable: true
    },
    toString: {
      value: [Function: toString],
      writable: true,
      enumerable: false,
      configurable: true
    },
    PROTO: {
      constructor: [Object],
      __defineGetter__: [Object],
      __defineSetter__: [Object],
      hasOwnProperty: [Object],
      __lookupGetter__: [Object],
      __lookupSetter__: [Object],
      isPrototypeOf: [Object],
      propertyIsEnumerable: [Object],
      toString: [Object],
      valueOf: [Object],
      __proto__: [Object],
      toLocaleString: [Object]
    }
  }
}

As you can observe, the Error instance has two properties, stack and message. message is the argument that we provide to the invocation of the Error constructor (Error function). Whereas stack is a property that is "auto-generated" (auto-populated) using:

  • The message property (that is, the argument that you pass to the Error constructor).
  • Current state of the call stack.

The stack property is nothing but a regular (normal) string. The composition of this string is as follows:

  • Name of the error. In this case, Error. If we were using another type of error, name would be that.
  • Followed by a colon and a space (that is, ": ").
  • Followed by the message that has been passed to the Error "constructor" function.
  • Followed by a string representation of the state of the call stack at the time that the Error instance was created.

Note that the enumerable attribute of both the stack property, and the message property is false. This means that if we were to print the properties of this Error instance using, say a for-in loop, we wouldn't see neither of these properties because they are not enumerable (i.e, their enumerable attribute is false).

After this information, let's display an Error instance on our terminal. We can display it with the console.log function, which simply calls the toString method on its argument and prints the result on the terminal. Start the Node.js REPL by running the node command on your terminal and enter the following code:

const e = Error('Some optional message');
console.log(e);

Output:

Error: Some optional message
    at REPL1:1:11
    at Script.runInThisContext (vm.js:133:18)
    at REPLServer.defaultEval (repl.js:484:29)
    at bound (domain.js:413:15)
    at REPLServer.runBound [as eval] (domain.js:424:12)
    at REPLServer.onLine (repl.js:817:10)
    at REPLServer.emit (events.js:327:22)
    at REPLServer.EventEmitter.emit (domain.js:467:12)
    at REPLServer.Interface._onLine (readline.js:337:10)
    at REPLServer.Interface._line (readline.js:666:8)

Let's add a property to the error and display it:

e.someProp = 'some value';
console.log(e);

Output:

Error: Some optional message
    at REPL1:1:11
    at Script.runInThisContext (vm.js:133:18)
    at REPLServer.defaultEval (repl.js:484:29)
    at bound (domain.js:413:15)
    at REPLServer.runBound [as eval] (domain.js:424:12)
    at REPLServer.onLine (repl.js:817:10)
    at REPLServer.emit (events.js:327:22)
    at REPLServer.EventEmitter.emit (domain.js:467:12)
    at REPLServer.Interface._onLine (readline.js:337:10)
    at REPLServer.Interface._line (readline.js:666:8) {
  someProp: 'some value'
}

When you add a property to an object, by default, the enumerable attribute of that property is true. Hence, the new property is present in the print-out of the error instance (which is simply the output of calling the toString method of the Error instance) in the form of {propertyName: propertyValue} which follows the value of the stack property.

Hence, with the presence of at least one enumerable: true property on the Error instance, the format of the print-out becomes:

  • The value of the stack property.
  • Followed by a pair of curly braces that contains the name and value of the properties of the Error instance whose enumerable attributes are true.

Let's add another property:

e.anotherProp = 'another value';
console.log(e);

Output:

Error: Some optional message
    at REPL1:1:11
    at Script.runInThisContext (vm.js:133:18)
    at REPLServer.defaultEval (repl.js:484:29)
    at bound (domain.js:413:15)
    at REPLServer.runBound [as eval] (domain.js:424:12)
    at REPLServer.onLine (repl.js:817:10)
    at REPLServer.emit (events.js:327:22)
    at REPLServer.EventEmitter.emit (domain.js:467:12)
    at REPLServer.Interface._onLine (readline.js:337:10)
    at REPLServer.Interface._line (readline.js:666:8) {
  someProp: 'some value',
  anotherProp: 'another value'
}

The new property is present in the print-out as well.

Now, let's try changing the value of the stack property of the Error instance. The enumerable attribute of the stack property is false. However, the writeable attribute of the stack property is not false. It is true. Hence, we can change the value of the stack property:

e.stack = 'asdf';
console.log(e);

Output:

[asdf] { someProp: 'some value', anotherProp: 'another value' }

As you can observe, the print-out changed, because we have changed the value of the stack property. However, note one difference from the format of the previous print-out. In the previous print-out, the value of the stack property was followed by a pair of curly braces that contains the name and value of the properties of the Error instance whose enumerable attributes were true. This time, everything is the same, except that the value of the stack property is surrounded by square brackets.

Let's see if we can get the print-out format to be like the previous print-out format again (that is, value of stack property is not surrounded with square brackets):

e.stack = 'asdf asdf';
console.log(e)

Output:

[asdf asdf] { someProp: 'some value', anotherProp: 'another value' }

e.stack = 'asdf\nasdf';
console.log(e)

Output:

[asdf
asdf] {
  someProp: 'some value',
  anotherProp: 'another value'
}

e.stack = 'asdf\n    asdf';
console.log(e)

Output:

[asdf
    asdf] {
  someProp: 'some value',
  anotherProp: 'another value'
}

e.stack = 'asdf\n    atsdf';
console.log(e)

Output:

asdf
    atsdf {
  someProp: 'some value',
  anotherProp: 'another value'
}

After some trial-and-error as shown above, we have discovered that if the value of the stack property (which is nothing but a regular string), contains a newline, followed by 4 spaces, followed by the characters a and t, then the portion of the print-out that represents the stack property is not wrapped in square brackets. Otherwise, it is.

The code that causes this is the following. It is located in the formatError function which is in lib/internal/util/inspect.js in Node.js source code.

// Wrap the error in brackets in case it has no stack trace.
const stackStart = stack.indexOf('\n    at', pos);
if (stackStart === -1) {
  stack = `[${stack}]`;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment