Skip to content

[Feature]: [Http] Provide DiscriminatedError interface for better ErrorFilter support #1025

@notaphplover

Description

@notaphplover

🚀 Feature Proposal

ErrorFilter feature is implemented on top of a single not-so-true premise:

"Given a an instance of type A, a instanceof A is guaranteed to be true".

While this is true more often than not, there're some caveats. Phantom dependencies might lead to multiple A modules, multiple A constructors, meaning a instanceof A might be false in some unlucky scenarios.

It looks reasonable to provide an alternative way to deal with this issue.

How do we deal with this problem in vanilla JavaScript?

This problem is not new, and neigther is a good enough solution to it: duck typing. This approach of asuming an object belongs to a given type if it has certain method / properties satisfying a set of conditions is my inspiration to solve the issue:

Proposal: DiscriminatedError

If we can provide a hint to efficiently duck type an Error, we can avoid instanceof usage while accomplishing a performance friendly validation:

const errorDiscriminatorReflectKey: unique symbol = Symbol.for('@inversifyjs/validation-common/errorDiscriminator');

function Discriminated(value: string | symbol): ClassDecorator {
  return (target: Function): void {
    // Set property associated to target at errorDiscriminatorReflectKey
  }
}

This way, adapters can fetch target error discriminator metadata. If found, adapters can keep a map of discriminators to Errors. When an error is thrown, we can attempt to get the associated error getting it from its discriminator via accessing the error type metadata. In the worst case, we can always fall back to the instanceof approach if no Error is found this way.

Error hierarchies

Consider the following errors:

@Discriminated('foo')
class FooError extends Error {}

@Discriminated('foo-child')
class FooChildError extends FooError {}

Internally:

  • FooError discriminator metadata is an array with one value: ['foo'].
  • FooChildError discriminator metadata is an array with two values: ['foo-child', 'foo'].
  • When a FilterError catches FooError errors, it's associated to the discriminator map to the key foo.
  • When a FilterError catches FooChildError errors, it's associated to the discriminator map to the key foo-child.

When a FooChildError is thrown, the adapter reads it's constructor error discriminator metadata and finds ['foo-child', 'foo'].

  1. The adapter first attemps to find filters for foo-child. If a FilterError handles FooChildError, it will be selected.
  2. If not, the adapter attemps to find filters for foo. If a FilterError handles FooError, it will be selected.
  3. If not, the adapter attemps to find Error filters with the current instanceof approach.

Motivation

It solves an ErrorFilter related feature as described in the proposal.

Example: Implementing a DiscriminatedError

const myAwesomeDiscriminatedErrorDiscriminator: string =
  'my-awesome-unique-discriminator-value';

@Discriminated(myAwesomeDiscriminatedErrorDiscriminator)
class MyAwesomeDiscrimatedError extends Error implements DiscriminatedError { }

Pros and cons

Pros

  • It can be implemented in a completelly transparent way. Developers implementing ErrorFilter classes for FooError don't need to care of whether or not the error is discriminated. Developers extending a DiscriminatedError don't need to be aware of its discriminated nature. Developers implementing a DiscriminatedError that extends a base class don't need to update the base class to be discriminated.
  • It's performant friendly: we can identify error filters in a few checks. We don't need to traverse the full type hierarchy.

Cons

  • ErrorFilter is now a feature user's need to care about implementation details. We could probably solve this enforcing every cached error must be discriminated, but the tradeof would be negative imo.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingenhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions