TritonDataCenter/node-verror

[Question] best practice handing verrors to users of a library

Closed this issue · 2 comments

I have a general question about the usage of this library when used in a developer facing library. I have a npm package that runs some tasks and gives the user an event emitter. The event emitter can listen for errors, and as an extra helper, if there is no error event listener, just throw the error:

// library's code
class MyEmitter extends EventEmitter {
  emitError(error) {
    if (this.listenerCount('error') {
      this.emit('error', error)
    } else {
      throw error
    }
  }
}
// user's code
import mylibrary from 'mylibrary'
const myEmitter = mylibrary.run()
myEmitter.on('error', error => {
  // can be either VError or nodejs Error instance
  console.error(error)
}

The situations I use verror for are

  • where I manually throw a custom error
  • if I catch an internal error that I want to attach more info to

My question is, what is considered the best practice in this situation? The user likely wants to log this error and possibly report it back to me, the library author if something goes wrong. However, I cannot assume they know what to do with a VError, nor should they have to care about the internal workings of the library. If they happen to print error.stack, they will just receive the most top level stacktrace, which is unhelpful. If not, they still receive a VError object, which is not a standard error object and is unwieldy to try to understand, especially when it is several levels deep. VError.fullStack(error) is the most user friendly output of this library, but I should not expect users of the library to know that is what they should run. Is it best practice to emit the string from VError.fullStack instead of an verror object to anything client facing?

I'm not sure I understand your question, but here's some background that may be helpful.

Generally, my goals for VError have been that:

  • If you don't know anything about VError, you (and callers) should be able to treat an instance of VError like any other JavaScript error.
  • If you (or callers) want to use some of the more sophisticated features (like causes and informational properties), you (and callers) need to use VError-specific interfaces (like VError.findCauseByName instead of checking the name property). On the plus side, we've built these interfaces so that you can always use them, even with Error objects that aren't actually VErrors.

If you decide to use VError-specific features in errors exposed by your module, you probably need to document that and tell callers to use VError.findCauseByName() if they ever care about what kind of error they're looking at and VError.info() if they care about informational properties. (Otherwise, they might try to use .name and get unexpected behavior.)

The rest of this comment is background I wrote up for myself to better understand the issue. I saved it here in case it's helpful for you or others (but I imagine much of it is stuff you know).


To make VErrors look as similar as possible to basic Errors, they have useful name, message, and stack properties. Traditionally, that was essentially all that JavaScript guaranteed about Errors. (I'm not sure what you mean by it being "unwieldy to understand". A lot of other libraries use their own error classes with their own properties, so I'm not sure it's reasonable to expect Errors not to contain other properties.)

These are the use-cases I think about for errors:

  • Basic message: A lot of programs (especially command-line programs) that encounter an error will just print it out (without a stack trace). In this case, you can just print "message", and it works the same as traditional Errors. When there's a cause chain, the messages are combined. This is an important use-case for us because it makes it easier to construct helpful error messages for command-line tools.
  • Making decisions programmatically based on the error: Programs with more complex error handling may inspect the Error object and take action based on it. Traditionally, you just have name and whatever other properties the callee provides, which usually vary by module and aren't always documented. You can still do this with VError, and it works the same way as long as the error has no cause. However, it's expected that you'll use VError-specific interfaces (VError.findCauseByName() instead of name and VError.info() instead of random other properties).
  • Print stack traces: Programs aimed at developers might want to log not just the message, but the whole stack trace. If you're not using cause chains, then this should work just as well with VError as any other error. I can see how if you're using cause chains and the caller doesn't know about VError, users might wind up seeing a stack trace that's not what you want (the wrapping error, instead of the wrapped error). I guess this is the case you're asking about? I think different cases might want different stacks (either the wrapped or the wrapping error's stack), and I don't think there's a way to address this without callers learning about this complexity. For what it's worth, we generally use bunyan for this, which knows about VError and prints the whole stack.

Basically, the second and third use-cases get tricky if you want to (1) use cause chains, and (2) don't want callers to have to know about VError. I don't think there's a great way to do that. However, I would definitely not recommend emitting a string of the stack instead of a VError object because that breaks two of the use-cases above.

I'm not sure if I've answered your question. Does that help?

Thank you @davepacheco for the extensive response! The third use case is in fact what I was describing (apologies if it was unclear). I wanted to make sure developers using the library could report back full errors to me if necessary.

As a solution, I did in fact start using bunyan. If a caller encounters a problem now, I ask that they send up the log file produced by bunyan, which will expand my VErrors. I still emit an error event, but after reading your use cases, callers dealing with the error will likely treat all errors universally. It therefore seems like it would fall under the first use case. I will emit the VError instance and leave a note in the docs.