Error "detail"

Now that I've taken a step back:

In theory, you could put this in the message and grep it, and most dashboards have a way to match based on message pattern. And in languages like Java that lack "detail", this is just standard.

However, structured debugging would be immensely useful, though. And I would especially like to see the proposed debugger.log also take this kind of message + detail combo.

  • In structured backend logging, you could put detail fields in separate keys. For instance, you could put it all in DETAIL_* fields in systemd.
  • In web consoles, if the same error is getting spammed, you could aggregate the messages while being able to expand it to see the details. (This kind of functionality, but for console.log, is one of my biggest pain points, as I do a lot of debugging of stuff that gets called a lot. And a per-detail histogram view would be very useful.)

Edit: filed a related feature request against Chrome DevTools: Chromium

I can see the value of this. However, its use sounds limited to browser environments that are able to provide the end-user with interactive debugging experiences. For backend servers, or CLI tools, or platform-agnostic libraries, you often don't get the luxury of having that kind of interactive experience with the errors you see.

For example, currently in our servers, whenever there's a network error with an outbound request, we throw an error whose error message looks something like this:

404 error received while sending a request to:
POST https://example.com/the/api/endpoint/2

(Request/Response bodies may be truncated if they're too large)
~~~~~~~~ Request Body ~~~~~~~~
{
  ....
}
~~~~~~~~ Request Headers ~~~~~~~~
Content-Type: ...
Accept: ...
~~~~~~~~ Request Body ~~~~~~~~
No entity exists with an id of "2"
~~~~~~~~ Response Headers ~~~~~~~~
Content-Type: ...

It's a really nice and clean way to read the error from stderr. If this error details proposal came out, it would basically be encouraging me to put all of this dynamic information into a details property on the error object instead of using the error message, and I could do that, but I don't see why I would - unlike with browsers, that would make my debugging experience worse, not better.

@dmchurch put it perfectly.

I disagree that this would only be useful in browser environments. When our AWS lambdas error, the error information is logged as JSON to CloudWatch. Cloudwatch has a UI which nicely renders JSON logs, and a custom query language which lets you search logs with filters like | filter error.detail.code = 400 and error.detail.request.body.user.type = 'guest'.

So in our case, if we stuffed all the info into message it would make it significantly harder to view and search errors.

This is not just true for AWS users, it also applies to anyone using Elasticsearch/ Kibana and other similar JSON-biased log aggregators/ search tools, as well as the system health dashboard aggregation benefits mentioned by @dmchurch

And as for the local development use case where you want the nice looking human readable output like the example you gave, you'd be covered by JSON pretty printing tools, e.g. npm start | npx pino-pretty or npm start | jq .. But of course there'd be nothing stopping you continuing to put everything in the message if that's the convention your team wants to follow.

1 Like

I'll point out as well that any argument that goes "there is no tooling support for structured data in an Error" is fairly specious. Of course there is no tooling support for structured data in an Error. There is, at present, no specification for structured data in an Error, which is exactly what this proposal hopes to rectify.

@theScottyJam, what gives you the impression that Node's default Error logging wouldn't pretty-print Errors with structured details in a logical way, if this proposal were implemented?

1 Like

I'm sure it would pretty print it, but it'll still be a step backwards in readability - especially if, say, the request body was JSON - we'd be printing a JSON object inside a string withing another JSON object. There's no way to pretty print that (especially considering all of the escaping that would have to be printed out in the nested JSON)

So, if we want to talk about error details as being the standard place to put information for tools like log aggregators to pick up and use, I'm cool with that. I can see value in that sort of thing. In my example above, I would continue to build a nice error message for humans to use, and I would additionally put some information in the error details that's intended for log aggregators to use. If we want to talk about error details as being a replacement for building nice error messages, that's where I (personally) start taking issue.

You're still making a lot of assumptions here - now it appears to be, "if this object gets pretty-printed, it will obviously be syntactically-valid JSON", which is a little absurd, but more to the point, it's irrelevant, as no one is suggesting removing the error message. Your entire argument appears to be "this is not useful for my particular use-case", when your current solution isn't being phased out. Do you understand how harmful that is to making useful progress in coming up with good ideas? If this solution doesn't affect you in either direction - and you have yet to make a case that you are affected by this potential change, even a little - then maybe consider that this is a conversation that you don't need to be a part of?

Perhaps we need to take a step back - what exactly are we trying to solve? What's being proposed is to add a new details object to errors as a place to put information - that's the solution, but what's the problem (in your own words?)

It's a little hard to discuss this when I'm not clear what the target is vs what are just some side thoughts related to the target, but are less important.

An engine knows how to pretty-print any kind of object; user code cannot. For example:

new Error("message", { detail: new Proxy({}, {}) })

If you call console.log(error), the engine can decide to print it as:

Error: message
  stack trace...
  detail: Proxy {
    [[Handler]]: {}
    [[Target]]: {}
    [[IsRevoked]]: false
  }

While no user code can do this.

2 Likes

To add to what @Josh-Cena said, my shortest version of the problem statement is:

I want throw new Error to have as much expressive power as console.log et al.

To be clearer, I don't care about the ability to concatenate raw strings by putting them in subsequent arguments; all I care about are the things that can't be losslessly represented by strings, since those are the things that force me to use the console.error; throw new Error pattern.

3 Likes

Ok, thanks - that's fair. You're wanting to increase the expressivity of errors by being able to put arbitrary values in an error message (otherwise it would take more work to represent those values as a string, and in some cases, as shown earlier with proxies, it may be impossible). This error details idea provides a solution to this problem.

On the other hand, my primary worry has been that, by using an error details proposal, we're losing expressivity in our errors by encouraging dynamic values to be places into a single object instead of being intermixed with the error message. (i.e. instead of saying "Received a 404 response while sending a request to example.com", the spirit of this proposal would have me instead do "Received a bad status code. Details { status: 404, targetUrl: "example.com" }" - the latter isn't bad, but the former is nicer. (You're right that I'm not forced to use the error details proposal if it's provided, but I still care about it, because it feels like it's encouraging people to build not-as-nice error messages - and as a consumer of other people's error messages I care about how the language encourages them to be structured).

You don't have to agree with my opinions or stance - that's fine. However, I think there's a way in which we can get the best of both worlds. We can provide a way to increase the expressive power of dynamic values in errors (letting you toss them in without worrying about stringifying them) while not making any sacrifices in the structure of the error message (which would make me happy).

What if, in addition to providing a "details" property on the error object for dynamic values to be placed, you can also provide "placeholders" in the error message itself, telling the language "this is where I want this particular detail property to show up when this error gets rendered". So something like this:

throw new Error('Received a $httpCode response while sending a request to $url', {
  details: {
    httpCode: 404,
    url: 'example.com',
  }
});

Node might render the above like this:

Received a 404 response while sending a request to example.com

Browsers would render it similarly, but would probably, additionally, let you view the details as an expandable object to see what the original values were in there.

If any of those detail properties were objects, or proxies, etc, they'd get rendered correctly as well, the same way console.log() renders this stuff.

And, you're also welcome to add additional properties to the details object without giving them an explicit spot in the error message - in this case, they can just show up after the error message. e.g.

throw new Error('I only substitute x: $x', { details: { x: 2, y: 3 } });
// Shows up as:
//   I only substitute x: 2
//   details:
//     y: 3

Or something to that effect. Every platform can choose how to render it according to what makes the most sense.

As a side bonus, this would make it easier for logging tools to aggregate similar errors - if they can get their hands on the original error message before substitution happens, they'd be able to easily compare the error message strings together.

1 Like

It's definitely a reasonable concern, you're not wrong at all - if you make a feature available, people will use it. And in this case in particular, it's worth remembering that not all environments will understand or expect the .details property, and like you say, Errors should not become useless because, in the worst-case scenario, some framework causes all Error.messages to become "An error was thrown by the application:". It's also a given that documentation is not a solution, we can't just say "let's tell people not to use details that way".

Put another way: right now, the lack of expressivity means everyone is using .message to communicate whatever needs to be described about any given error, because that's the only option. The corollary to that is that an Error consumer can be certain they've recorded whatever needs to be described just by storing the .message field.

So, if we want to make sure people don't abuse the details feature and, in so doing, break consumer expectations of the .message field, let's reduce its expressivity. My problem statement is that "Error should be as expressive as console.log", but this proposal currently goes further than that by expressing the details as an object with named fields.

So, let's restrict the detail property to be arrays only.

It still hasn't, technically, lost any expressivity, because you could just put an object with named fields as the single element of the details array, but that requires developer intent to break the standard. And that's fine, they should be able to, but it shouldn't be the norm. Having details be an array also means that placeholders become much simpler - they can follow regex replacement placeholders and be $0, $1, etc.

But there's another thing that restricting it to arrays buys us, and I'll show rather than tell:

const httpCode = 404, url = 'example.com';
throw Error.new`Received a ${httpCode} response while sending a request to ${url}`

// The Error.new() template function calls the following:

new Error("Received a $0 response while sending a request to $1", {
    detail: [httpCode, url],
});

// Which results in an Error object with the following properties:

{
    message: "Received a 404 response while sending a request to example.com",
    rawMessage: "Received a $0 response while sending a request to $1",
    detail: [404, "example.com"],
}

// and could be displayed in the same manner as:

console.error(
    "Received a ", httpCode, " response while sending a request to ", url);

I dunno about you but I love that syntax. And, being a static method, Error.new is polymorphic to subclasses, so you can use any MySpecializedError.new in the same way.

The placeholder replacement only takes place for array indices that are valid to the length of the detail array, so if the array is length 0 or omitted, no placeholders are replaced and the message property will be the same as the message parameter passed to the constructor, regardless of if there are any $0 sequences in the message.

If there are any valid placeholders, though, they will get replaced by the string coercion of that array value, the same as string templating and String.raw does. In that case, there will be a rawMessage property with the string passed as the message parameter to the constructor. I figure the Error prototype will have a get rawMessage() { return this.message; } getter defined, so that knowledgeable clients can always use it to get the raw value of the message property, whether placeholder replacement was performed or not.

What do you think? And, for that matter, @mmkal, what do you think about the reduction in scope of the feature, restricting the detail property to array values?

2 Likes

Heeey, that's pretty nice, I like it.

We'd want to make sure that this "new" template function behaves correctly when it's inherited by a subclass, i.e. it should always attempt to create an instance of Error using the class it is found on.

This could become a little problematic if a subclass chooses to change the function signature of the constructor, but I'm fine with that, it just means you cans use the new() function on that particular subclass unless they specifically override new() to work on it.

1 Like

If they change the construct signature of their Error subclass such that you can't call it with (message, options) as its argument, then yes, you would not be able to use the static Error.new`` to construct it. However, if someone has overridden the constructor to remove the message parameter, it's probably because they're getting the message in some other way... which means they're unlikely to use the .new template function to try and pass it in :joy:

But yes, I'd say that including the requirement that Error.new instantiate its receiver (aka, that it has new this() semantics rather than new Error() semantics) is a good idea.

Also, of course, the static method doesn't have to be called new - it may want a different name on account of the call signature (templateStringArray, ...interpolations) differing from the class's construct signature (message, options). A better option might be of or from, to parallel Array.of and Array.from et al. Figuring out the naming can come later, though - the primary question is, are there any delegates that think this is an idea worth championing?

1 Like

That sounds handy, but I would personally want it to be an add-on proposal. I think detail should be able to take any form. I still just want to be able to write

throw new Error('thing failed', {detail: {foo: {bar: q}}})

But a special case for when it happens to be an array sounds useful too.

I'd like for it to be able to accept any form as well, but remember, the process of getting a proposal into ECMA262 is a matter of gaining consensus. Proposals that have a more restricted scope are more likely to get support, because ECMAScript is a "no takebacksies" language - if TC39 says detail can be any form, then it can be any form, forever. On the other hand, if TC39 says detail can only be an array, the door is open for (a) a browser to go beyond what the spec requires, or (b) a followup proposal to widen the definition to allow for objects as well. In your example, you could replace that line with:

throw Error.new`thing failed: ${{foo: {bar: q}}}`;

You've already got some amount of consensus, at least among us non-delegates out here in the cheap seats, if you modify your proposal to specify arrays-only. Don't let your attachment to one particular solution turn an "everybody says this is a good idea" into a "some people have concerns", because that's how TC39 proposals die.

That being said, @mmkal, I'm curious to hear how you would summarize the problem statement. Would your summary be different from @dmchurch?

1 Like

Oh, thank you, good catch! I am also curious to hear this answer :smile:

Re: my summarized problem statement. I like @dmchurch 's one, but maybe a variant that doesn't reference console.log:

The Error class doesn't have a built-in, standard way to add supplemental information to thrown errors, without subclassing or stuffing stringified data into its message.

Re: consensus/no-take-backsies: I agree, and maybe eventually I could fall in line, but I also suspect that simplicity of implementation, lack of edge cases, and precedent are important if we want to get this approved. Just adding this.detail = options.detail to the Error constructor is about as simple and non-controversial as it gets. Whereas the interpolation proposal has many tricky things to figure out:

  • speccing out what happens to a static property in subclasses
  • deciding how to format the detail parameters (in my {foo: {bar: 1}} case, will we see [Object object]?
  • adding another two properties to Error, increasing likely clashes (rawMessage and new)
  • figuring out how to handle messages that just so happen to match the placeholder format (would new Error('Product must have price > $0', {detail: [123]}) be problematic? Maybe this objection is bogus?)

All of those are solvable, but each could be a sticking point gaining consensus. Then documenting how to use, because of this extra complexity, also becomes harder. (Also, I may have misunderstood, please let me know if I've got the wrong end of the stick with any of the above)


So, trying to put my committee member hat on, I think I would prefer the original, simpler, idea.

Having said that, I still like what you're proposing more than nothing, so I would lend it my (meaningless) vote if the original idea were rejected outright.

@ljharb since you're a delegate who has engaged with this, any thoughts? Did you find the reasons given why cause isn't suitable convincing? Any initial sense of whether new Error('thing failed', {detail: {code: 123}}) or Error.new`thing failed with code ${123}` seems more likely to get through?

2 Likes

Well, at the very least, I think there's a problem statement that we agree on (which is good enough for stage 1). There's multiple potential solutions here that have different pros and cons. We aren't required to decide on the solution we want - that doesn't come until later stages.

That being said, I'll respond to a couple of your concerns.

Technically it's platform specific, so a platform could do that if they wanted. But in practice, I'd say that this would go against the spirit of the proposal. If that's how platforms are going to choose to render it, then this proposal would become basically meaningless on that specific platform - we asked the platform to render the contents of an object in a human readable format, and it just showed us the same garbage we could have come up with ourselves.

This concern isn't specific to this tagged template solution either. In your original proposal, if you put an object as one of the values in the details object, will the platform choose to render that as "[object Object]". The answer is the same - the platform could, but that would be silly and go against the spirit of the proposal.

(Also remember that when dealing with tagged template literal, it's very normal for the template tag to deviate from the normal stringifying behavior that untagged template use on their interpolated values)

If you use the details feature, then you're opting in to having dollar signs mean something special in your string. If you create the error the "norma" way, with Error.new, then you won't ever have this issue because Error.new will take care of escaping for you. If you manually construct the error, you'd just have to escape it yourself.

It's not too different from how some character sequences in a string passed into console.log() have special meaning.


All this being said, your concerns are valid (and it's why I'm not responding to all of your points, because, for the rest of them, I have nothing more to say then "yep, that's a valid concern"). It's a good thing that, either way, a single concrete solution isn't required at this point (but I still find it nice to have ideas of potential solutions(s) so we know what kind of cost we're looking at to solve a particular problem.

1 Like

The thread is long and I haven't fully read it, so excuse me if this was already answered, but what is the intended purpose for the supplemental information?

In particular is it meant to help catching code to make decision about the error, or for diagnostics purposes when the error is reported?

The reason I ask is that the former is somewhat contentious. And the latter does not strictly require the supplemental information to be attached as properties.

In particular at my company we're pretty wary of errors carrying any public data beyond related errors (cause / aggregate errors). We suppress public stack traces and prevent errors with extra stuff on them from going through some boundaries. Annotating an error with all kinds of diagnostics information (which includes the stack) is definitely encouraged, it's just not visible except to the reporting system / top level code which has the power extract this information. FWIW, we consider error cause or aggregated errors to also be better served as private diagnostics information, but without native support for this kind of attached info, we accepted adding them as properties instead.

1 Like