Skip to content

Error subclasses: how are they checked? #265

Closed

Description

@ljharb

(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)

Activity

  1. ljharb commented on Jul 8, 2022

    @ljharb
    MemberAuthor

    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.

  2. Jack-Works commented on Jul 8, 2022

    @Jack-Works
    Member

    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.

  3. theScottyJam commented on Jul 8, 2022

    @theScottyJam
    Contributor

    @ljharb With option 3, if I were to do this:

    export class MySyntaxError extends SyntaxError {
      customProperty = ...;
    }

    and then a user of my library tried to do this:

    function handleError(error) {
      return match (error) {
        when (${MySyntaxError}): console.error(`Error occured with value: ${error.customProperty}`)
        ...
        default: console.error('Unknown error!')
      };
    }

    This code will act like it works just fine, until a normal SyntaxError gets passed in, at which point it'll go down the wrong branch and end up logging Error occurred with value: undefined. Is this a correct understanding? This seems like an unfortunate foot-gun is subclasses inherit the pattern-matching behaviors like this. The same applies for inheriting non-error classes as well, but that tends to be less common.

  4. Jack-Works commented on Jul 8, 2022

    @Jack-Works
    Member

    I'm worrying we need to consider all commonly subclassed features:

    • Map
    • Set
    • Array
    • Error
  5. ljharb commented on Jul 8, 2022

    @ljharb
    MemberAuthor

    @Jack-Works what you wrote yesterday is option 1. However, option 3 also works cross-realm, and all options will work with proper subclasses, since they'll have the proper slots.

    Everything that's not Error already will work, because a proper subclass will have the internal slot of the base. It's just that Error subclasses don't have a unique slot - just a slot shared with the base Error.

  6. ljharb commented on Jul 8, 2022

    @ljharb
    MemberAuthor

    @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.

  7. theScottyJam commented on Jul 8, 2022

    @theScottyJam
    Contributor

    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.

    Could there perhaps be a mix? If you're creating an instance of an Error, or of a built-in subclass of an error, then it uses option 3 and stores some value unique to that builtin-type in an internal slot, but if it's a userland subclass we store new.target?

  8. ljharb commented on Jul 8, 2022

    @ljharb
    MemberAuthor

    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.

  9. Jack-Works commented on Jul 8, 2022

    @Jack-Works
    Member
    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?
    }
  10. ljharb commented on Jul 8, 2022

    @ljharb
    MemberAuthor

    @Jack-Works yea, if we go with #265 (comment) they would all match as expected.

  11. tabatkins commented on Jul 11, 2022

    @tabatkins
    Collaborator

    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.)

  12. ljharb commented on Jul 11, 2022

    @ljharb
    MemberAuthor

    Agreed. I'll prepare a PR to make that change.

  13. Jack-Works commented on Jul 12, 2022

    @Jack-Works
    Member
    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.

  14. 5 remaining items

  15. treybrisbane commented on Jul 13, 2022

    @treybrisbane
    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.

  16. Jack-Works commented on Jul 13, 2022

    @Jack-Works
    Member

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

    I absolutely agree. Subclassing error is a widespread use case, it is worth the complexity of supporting it.

  17. added a commit that references this issue on Jul 13, 2022
    8b4a1f6
  18. ljharb commented on Jul 13, 2022

    @ljharb
    MemberAuthor

    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.

  19. added a commit that references this issue on Jul 13, 2022
    a478755
  20. theScottyJam commented on Jul 14, 2022

    @theScottyJam
    Contributor

    And, to be clear, is that change going to just make subclasses of Error work, or will it allow subclasses of all built-in types? (From my reading, it looks like the former?)

    I don't feel overly strong about other built-in types, since I know they're rarely subclasses and really shouldn't be, except that it would be nice for consistency sake to make them all behave the same when subclassed.

    Also, I know that, for example, HTMLDivElement is a browser API and not part of the core of EcmaScript, but I suspect browser APIs will follow whatever pattern we set here with their built-in types. And, I know they actually encorage subclassing HTMLDivElement (and all other HTML element classes), as that's the only way to make custom elements. It would be nice if pattern-matching against one of those userland subclasses just worked as well.

  21. ljharb commented on Jul 14, 2022

    @ljharb
    MemberAuthor

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

  22. bathos commented on Jul 14, 2022

    @bathos

    I suspect browser APIs will follow whatever pattern we set here with their built-in types. [...] It would be nice if pattern-matching against [userland subclasses of HTMLElement] just worked as well.

    Yep, Web IDL (at internally create a new object implementing interface) deliberately replicates the same observable behaviors as OrdinaryCreateFromConstructor, just using different spec terminology (because they don’t share the [[Intrinsics]] & %Name% syntax pattern). It seems likely that the [[NewTarget]] slot logic could/would end up incorporated similarly because it was expressly stated in the past that alignment was the intention.

    HTML uses a special construct-algorithm override in place of the usual Web IDL steps via the [HTMLConstructor] extended attribute but delegates to the same at step 7.1. Because these constructors-when-used-as-super already have unique handling, there might be a good opportunity there to address the custom element scenario in particular with “automatic” subclass matching (esp. given the agent has privileged knowledge of whether instances are valid; there is effectively a kind of built-in branding for these subclasses). The only other commonly subclassed platform interface constructor I can think of is Event, but I don’t personally think implementing branded matchers is a big burden for that scenario — there’s already a need for special care along those lines when doing subclassing it.

  23. ljharb commented on Jul 14, 2022

    @ljharb
    MemberAuthor

    @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.

  24. added 2 commits that reference this issue on Jul 14, 2022
    9a7daa4
    8246ca8
  25. tabatkins commented on Jul 15, 2022

    @tabatkins
    Collaborator

    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).

  26. added 2 commits that reference this issue on Jul 15, 2022
    c9196df
    c2f5bcf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions