Replace Error.isError with instanceof Error in createErrorDetails - #2192
Conversation
Error.isError is Baseline limited availability and is not polyfilled, so on browsers at the low end of the supported range the call threw TypeError inside the function whose job is to describe an error. Closes aws#2181
366f6d1 to
a7b8779
Compare
|
Nothing here enforces the floor, and nothing in this PR can. I've filed #2237 with the investigation. On the tests: they are a regression guard for this function, not a guard for the class. Reverting line 52 back to Error.isError fails all three of them, so they hold that one function in place. Each covers something distinct: serializeCause is only reached |
kmcginnes
left a comment
There was a problem hiding this comment.
Verified this works. I reproduced the crash on the pre-fix commit in Chrome with Error.isError deleted, and it's gone here. With the method present, output is identical on both commits.
Approving. Three non-blocking notes below.
You convinced me on the test block
I was going to ask you to drop describe("browsers without Error.isError") and the helper, on the grounds that nothing in createErrorDetails reads Error.isError anymore so the deletion is inert. Your reasoning is better than mine. Two points I'd under-weighted: within that block the cause case is the only one reaching the second call site at line 66, since serializeCause needs a cause present. And you checked the shorter vi.spyOn version and found concrete reasons it fails, including that @vitest/expect calls Error.isError internally so the mock breaks toStrictEqual from the inside. Documenting the alternative you rejected is what settled it. Keeping them.
Two small things in that block, neither blocking:
includes the nested cause chain as databuilds a single-level cause, so it never reaches the recursion atcreateErrorDetails.ts:75. It also nearly duplicates the name of:67, which does build a chain. Rename it, or add amidlevel.withoutErrorIsErrorignores theReflect.deletePropertyresult. On a runtime withoutError.isErrorthe delete is a no-op and all three tests pass having exercised nothing.expect(descriptor).toBeDefined()before the delete covers that.
Worth folding into the description
The impact is worse than Error.isError is not available in all supported browsers #2181 describes, and its repro steps don't actually work:
- An unreachable endpoint throws
TypeError, which theServerConnectionErrorbranch catches at line 17, before the guard. Reaching it needs aDOMException, for example a listener that accepts and never responds against a fetch timeout. - On the old code the whole route was lost to the app error boundary, not just the error panel. And on the write queue path a
QuotaExceededErrorescaped asUncaught (in promise), so the "Changes not saved" indicator never appeared at all. A user with full storage got no warning that nothing was being saved.
Wording
"No user-visible output changes on browsers that do have the method" isn't quite absolute. Object.create(Error.prototype) gives Unknown Error / {} before and Error / "" after, since Error.isError tests the internal slot while instanceof only walks the prototype chain. Nothing in the app produces such a value, so no code change needed, but worth narrowing since this becomes the squash commit message.
Before merge
Needs a rebase. The branch is 10 commits behind main, and the red install-and-test check is the useSchemaSync flake fixed by Stop flaky assertions in proxy server and schema sync tests #2220, which landed after this branch's base. A re-run at the current head would still use the pre-fix base.
Thanks for Enforce the baseline-widely-available browser floor #2237. That's the right home for the enforcement question, and it's out of scope here.
|
Fair on both counts, and the second one turned out to be bigger than it looked. On enforcement: you're right that nothing here enforces the floor, and nothing in this PR can. I've filed #2237 with the investigation. Two things worth knowing up front: oxlint 1.82 can load ESLint plugins via , so this is reachable without adding ESLint, and pinned to flags and Web APIs. Running the candidate stack over also turned up two violations already shipping on — in and , which Firefox 114 lacks. So the class is live right now, not just historical. I've also commented on #2180, because its planned permits and , both Safari 17.4; closes those for 5 more type errors. On the tests: they are a regression guard for this function, not a guard for the class. Reverting line 52 back to fails all three of them, so they hold that one function in place. Each covers something distinct: is only reached when a cause is present, so the cause-chain case is the only one exercising the second call site at line 66, and the case is the issue's reproduction. I did try replacing the reflective deletion with a mock, which would have been shorter. Two things ruled it out. calls internally, so a test-scoped spy is still installed while assertions run — mocking it to throw fails from inside , not from the code under test. And stops compiling under the narrowed that #2180 introduces, which would mean shipping a cast into the branch whose purpose is to unblock that narrowing. The current helper avoids both by scoping the deletion to the single call and keeping the property name out of the type system. Happy to trim to fewer cases if you still think three is too many, but I'd keep at least the cause-chain one since it's the only coverage of the second guard. |
Description
createErrorDetailsnarrowed unknown values withError.isError, which is Baseline limited availability and is not polyfilled here. On a browser without the method, the call threwTypeError: Error.isError is not a functionfrom inside the function whose job is to describe an error, so the error panel failed instead of rendering the message.Both call sites now use
instanceof Error, which is sufficient here:ServerConnectionError,NetworkError,ZodError,QueryValueError) all matchErrorsubclasses and are evaluated first, so their output is unchanged.DOMExceptionsubclassesError, so the IndexedDB failures that reach the persistence and migration paths still narrow correctly.No polyfill was added, which matters because #2180 removes
core-jsentirely.How to read
createErrorDetails.ts— the core change: two guards, one increateErrorDetailsand one inserializeCausecreateErrorDetails.test.ts— three tests covering the absent-method path, plus thewithoutErrorIsErrorhelper. It deletes the property reflectively rather than referencingError.isErroras a typed property, so the test still compiles once Stop using Iterator helpers so core-js can be removed #2180 narrowslibtoES2024docs/agents/testing.md— supporting: records that absent-global test pattern as a conventionValidation
TypeError: Error.isError is not a functionatcreateErrorDetails.ts:52, and pass after it.pnpm checkspasses.pnpm testpasses at 2675 tests across 220 files. The branch is rebased onto currentmain, so this includes the Vitest 5 upgrade from Update Vitest to version 5 #2166.#2180unblock rather than assuming it: settinglibto["ES2024", "DOM", "DOM.Iterable"]and runningpnpm check:typesreports no remaining errors attributable to this call site. Thattsconfig.jsonedit was reverted and is not part of this PR.Related Issues
Check List
pnpm checkspasses with no errors.pnpm testpasses with no failures.