Interpolating objects in error messages (e.g. like console.error)

Currently when creating custom errors it is quite difficult to insert other data, for example consider a trivial example of a simple assertion function that checks if it's value is a string:

function assertIsString(value) {
  if (typeof value !== "string") {
    throw new TypeError(`Value was not a string`);
  }
}

assertIsString({ x: 10, y: 20 });

This is fairly weak error message though, upon seeing the message "Value was not a string" little information is provided as to what was not a string.

Now we can try to interpolate values into our error message e.g.:

throw new TypeError(`${ value } was not a string`);

But as expected, the default stringification for most objects is just [object Object], not particularly useful. One could use JSON.stringify but this again only works on objects that are valid JSON and will break completely in the precense of object cycles.

Now a number of host environments already support interpolating objects into messages via console.error.

For example in Node for the following code we get a nicely formatted string:

const o = { x: 10, y: 20 };
console.error(`%o was not a string`, o)
{ x: 10, y: 20 } was not a string

In Chrome Devtools this is even better, as not only does it pretty print the object, but it is expandable and explorable as an object:

image

It would be cool if error messages supported something similar, so that when an error is shown it has nicely formatted and explorable objects.

Now there's a couple ways this could be done, either we adopt the console specifcation's formatter syntax, i.e. we would do something like:

class Error {
  constructor(message: string, ...data: any[]) {
    const stackMessage = HostFormatMessage(message, data);
    // etc
  }
}

throw new Error(`Expected %o to be a string`, value);

Or perhaps a more generic mechanism for a "formatted" message:

class Message {
  static create(strings: TemplateStringsArray, ...data: any[]): Message {
    return new Message(parts, data);
  }

  #strings: TemplateStringsArray;
  #data: any[];
  
  constructor(strings: TemplateStringsArray, data: any[]) {
    this.#strings = strings;
    this.#data = data;
  }

  toString(): string {
    return HostFormatMessage(this.#string, this.#data);
  }
}

throw new Error(Message.create`Expected ${ o } to be a string`);
// Could even work with other APIs e.g. all the console methods, in a cross-platform way:
console.log(Message.create(`Saw value ${ o }, adding to processing queue`));

I'll note that in Node, if you attach properties to the error object, they'll show up when the error is thrown. This doesn't seem to be the case in Chrome though, not sure about other browsers.

> const myError = new Error('Value was not a string')
undefined
> myError.value = { x: 10, y: 20 }
{ x: 10, y: 20 }
> throw myError
Uncaught Error: Value was not a string
    at REPL9:1:17
    at Script.runInThisContext (vm.js:133:18)
    ...
    at REPLServer.Interface._line (readline.js:666:8) {
  value: { x: 10, y: 20 }
}

Perhaps the Error cause proposal could help with that particular example?

throw new Error('Value was not a string', { cause: value })

I'll note that Node supports a very, very small superset of the console spec for its util.format. The only differences it has with when a format string is passed is this:

  • %% gets reduced to a single % without consuming any arguments. (It's odd the console spec doesn't account for this.)
  • %j results in the object displayed as JSON (as if via JSON.stringify, but with circular references replaced with the string "[Circular]").
  • %c is ignored.