proposal-pattern-matching: Error subclasses: how are they checked?

(from #263)

Checking the [[ErrorData]] slot ensures it’s an Error or an Error subclass.

However, when someone matches against, say, TypeError, we only want a TypeError to satisfy this.

Here are some options:

  • do a Get of name on the constructor, do a Get of the constructor on the value and a Get of name on that constructor, and compare them (most observable, least robust)
  • do a Get of constructor on the value, and SameValue to the constructor (better than the previous, but still relies on an observable and forgeable lookup)
  • add a new subclass-specific internal slot to NativeError and AggregateError, and check for it on the value (most robust, but adds a new capability that isn’t already present)

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 26 (22 by maintainers)

Commits related to this issue

Most upvoted comments

@bathos i got some feedback that the slot approach won’t likely be palatable to implementations, so i’m working on an alternative approach that walks the prototype, supports intrinsics from any realm, and gets .constructor to do so. Both approaches should theoretically Just Work with web subclasses as well - HTML would only have to do extra work if it wanted things to work cross-realm for web builtins.

class MyError extends Error {}
class AnotherError extends Error {}

match (new MyError) {
    when (${AnotherError}): can this match?
    when (${MyError}): can this match?
    when (${Error}): can this match?
}

if we can support this, it will be good.

FWIW, as a rando developer, I feel that error type matching is one of the primary use cases for pattern matching.

As an example, I frequently write code at the API layer that converts domain errors to JSON responses, and that code is almost always something like “if the error is of this type, return this response, else if it’s that type, return that response, …”. Pattern matching would be a big improvement for me here so long as I can easily differentiate different error types.

If the above example didn’t work as-is in the final version of pattern matching, I’d personally be very, very sad.

Yeah, #3 is probably what I’d expect. It does indeed mean that subclasses will test as correct, but that’s identical to how any other class’s subclasses will work, so that seems expected - classes inherit their static members from their superclass as well, which will include the [Symbol.matcher] method. There is literally no way to avoid this without bypassing the class system entirely (some sort of registry?) which seems bad. (Subclassing is, itself, often bad, but that’s something every coder needs to learn for themselves, apparently.)

My preference is the third, because it’s the most robust.

Userland subclasses, of course, can and should use a private field and #brand in obj, which has the same semantics as option 3.

Yup, the #279 approach will automatically work for WebIDL objects, in fact, and userland subclasses of HTMLElement, since it proto-walks - so long as you don’t want to do anything fancy, when (${MyNewElement}): ... will Just Work.

It doesn’t even require any new special-cases unless we specifically want cross-window versions of WebIDL-defined classes to also match (which we might, I dunno, but if we do it’s a pretty easy tweak to WebIDL to duplicate what #279 is doing for all WebIDL-defined classes as well).

@theScottyJam #273 only makes builtin error subclasses work. #279 would make userland subclasses of any builtin constructor (except the primitives) work.

I’ve put up #279, on top of #273, which combined use instanceof semantics whenever the original constructor was not a builtin. I strongly dislike instanceof semantics, but I can’t think of a more appropriate choice, sadly.

I don’t think it would be a good idea to do observable lookups in the builtin matchers, so I still think https://github.com/tc39/proposal-pattern-matching/issues/265#issuecomment-1178612254 would be the only way that could work. We’d also have to implement that - along with a new slot - for every kind of builtin.

That seems complex enough that it might obstruct the entire proposal 😕

I suppose we could do that. Each builtin subclass would check new.target, and if it was SameValue, it’d set the internal slot to the string name, and if not, it’d set it to new.target. That would work cross-realm with all builtins, and same-realm (the only option) with userland subclasses. It’d get a bit more complex to handle in Error itself, but it’s doable.

@theScottyJam with all three options, your subclass will be treated as a SyntaxError, since it will have the proper slots.

You are certainly correct that your subclass would have to define its own custom matcher if you wanted to be able to robustly match against your subclass instances.

Perhaps another choice would be to store new.target in an internal slot, and then access that - but then Error subclasses would no longer work cross-realm.

Current algorithm (as I wrote yesterday):

  • Requires the [[ErrorData]] internal slot.
  • Check if the value.constructor equals this, if so, it matches
  • Check if the value.constructor.name equals this.name, if so, it matches.

This algorithm automatically works on all sub-classes (even from the user land) of the Error class, and it also works cross-realm.