Skip to content

Conversation

@WilcoFiers
Copy link
Contributor

@WilcoFiers WilcoFiers commented Aug 28, 2025

Part of #4860

If a check or matches method throws, catch the error and add a node with an error-occurred none check. This way incomplete results are always reported.

This only happens for axe.run / axe.runPartial. rule.runSync() still throws if an occurs in a rule or check.

Comment on lines -746 to -752
description: 'An error occured while running this rule',
message: err.message,
stack: err.stack,
error: err,
// Add a serialized reference to the node the rule failed on for easier debugging.
// See https://github.com/dequelabs/axe-core/issues/1317.
errorNode: err.errorNode
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I got rid of most of these, and updated how error works:

  • error is now serialized so that it can be carried across frame boundaries
  • description is clobbered by the reporter. It never even left axe (plus it has a typo)
  • message and stack are duplicated on error
  • errorNode wasn't getting pulled through nodeSerializer

ErrorNode in particular was a problem. Because it wasn't serialized, it A. doesn't have the frame selector, and B. leaves the _element property on it, which doesn't play well with serialization (axe Extension would completely error out trying to serialize this).

@WilcoFiers WilcoFiers marked this pull request as ready for review September 2, 2025 15:31
Copilot AI review requested due to automatic review settings September 2, 2025 15:31
@WilcoFiers WilcoFiers requested a review from a team as a code owner September 2, 2025 15:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements error handling for axe-core checks and rules by catching errors and converting them to incomplete results instead of failing completely. This allows the scanner to report what it could successfully check while noting where errors occurred.

  • Introduces a new SupportError class to wrap errors with additional context
  • Adds error handling to rule matching and check evaluation phases
  • Creates a dedicated "error-occurred" check type for incomplete results

Reviewed Changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/core/utils/support-error.js New SupportError class for wrapping errors with context
lib/core/utils/serialize-error.js Utility for serializing errors to JSON
lib/core/base/rule.js Updated to catch errors and wrap them in SupportError
lib/core/base/audit.js Added error handling in audit run and after phases
lib/core/utils/merge-results.js Support for merging error information from frames
lib/checks/shared/error-occurred.json New check definition for error cases
test/integration/full/error-occurred/* Integration tests for error handling
test/core/utils/* Unit tests for new utilities

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

if (typeof err !== 'object' || err === null) {
return { message: String(err) };
}
const serial = { ...err }; // Copy all "own" properties
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ...err is going to mean that any (potentially non-serializable) properties of the original error will stay as-is in the returned object, no? Doesn't this defeat the purpose? I'd have expected that whole point of this function would be to make sure the following test case passes:

  it('should not include unserializable errorNode properties', () => {
    const baseError = new Error('test');
    const errorNode = new axe.utils.DqElement(document.documentElement);
    const supportError = new axe.utils.SupportError({ error: baseError, errorNode })
    const serialized = serializeError(supportError);

    assert.isUndefined(serialized.errorNode?._element);
  })

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(presumably by only passing through properties known to be serializable and specifically handling errorNode by passing it through nodeSerializer.dqElmToSpec)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's not perfect. I'm not sure there's an easy way to ensure this. Considering this is internal, this seemed sufficient to me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you leave an unserializable property on the "serialized" error, doesn't that defeat the purpose of the PR and still leave you in the same place as you were originally, where you'll get errors when trying to pass an axe result containing the partially-unserializable error across a serialization boundary?

If we're satisfied with not having errorNode and instead relying on the node in the synthetic check result, I think the "easy way to ensure this" is to replace { ...err } with {} here and add the non-errorNode properties of RuleError to the list of props on L12. (maybe also add code to the list, it's common enough in things that could be error causes)

];
const node = errorNode || new DqElement(document.documentElement);
return Object.assign(new RuleResult(rule), {
error: serialError,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue suggests that this should also include errorNode here (for compatibility with existing error handling behavior). Note that if we do this, we'd need to pass it through nodeSerializer at frame boundaries.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's why I chose not to do that. It's a pain to properly serialize, with little benefit. See PR description.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think "it's a pain to do" is a good enough reason to break backcompat with existing users' error handlers. The specification in the #4860 (in "What to change" 1) mentioned "In addition to using an error and errorNode property" and "the error and errorNode properties should continue to be set." I think it'd be better to keep it for backcompat.

I'm not sure what you're referring to by "See PR description", I don't see anything relevant there - did you mean this comment? That comment mentions that it's a problem that errorNode isn't going through nodeSerializer like it should, which I agree with, but I don't think it justifies why breaking the existing property is a preferable solution to "update it to go through nodeSerializer". There aren't that many places where we need to do nodeSerializer.mergeSpecs at frame boundaries, I'd expect it to be a reasonably contained change to update those places to make sure they also mergeSpecs on error nodes.

Looking at the current code (before this PR) that creates errorNodes, I'm actually a bit surprised/confused that we see any current scenarios where they contain _element properties that confuse serialization; the only places that appear to do an initial set of errorNode are in check.js where they come from a nodeSerializer.toSpec output. It makes sense that the original values would be missing ancestor frame information, but it surprises me that they'd end up with _element properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree it's a breaking change:

  1. It's already an optional prop: not every method sets it, so not having it can't possibly break things.
  2. It's an undocumented property, changing those isn't a breaking change.
  3. If this was breaking, than so would removing message and stack be.

errorNode is also much buggier than I realized it was when I wrote the issue. It doesn't work across frames, and it doesn't run through NodeSerializer. Everything errorNode does can now be done in a better way. errorNode is a "feature" that has never worked, and that we keep making mistakes with. Better to drop it IMO.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think 1 is a bit of a sketchy argument - I think if someone had a UI for errors that attempted to display which node an error occurred at, and that UI worked fine for errors in checks in <4.11 but stopped working for errors in checks in >=4.11, I think they could reasonably argue that the change broke them.

But points 2, 3, and that it's already really buggy anyway are all fair enough. I verified that I don't think we have any products with such an error UI already (we do have one product that parses errorNode if it's present, but it doesn't do anything with it). I can live with dropping it since it sounds like you feel strongly about it, it's not a hill I'm going to fight further on.

@straker straker added this to the Axe-core 4.11 milestone Sep 11, 2025
@WilcoFiers WilcoFiers requested a review from dbjorge September 24, 2025 10:51
Copy link
Contributor Author

@WilcoFiers WilcoFiers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve Dan's changes

Copy link
Contributor

@dbjorge dbjorge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving + security reviewing everything except my commit (which Wilco approved above)

@dbjorge dbjorge merged commit 32ed8da into develop Sep 24, 2025
23 checks passed
@dbjorge dbjorge deleted the error-handling branch September 24, 2025 21:19
straker added a commit that referenced this pull request Oct 9, 2025
##
[4.11.0](v4.10.3...v4.11.0)
(2025-10-07)

### Features

- add RGAA tags to rules
([#4862](#4862))
([53a925a](53a925a))
- **aria-prohibited-attr:** add support for fallback roles
([#4325](#4325))
([62a19a9](62a19a9))
- **axe.d.ts:** add nodeSerializer typings
([#4551](#4551))
([a2f3a48](a2f3a48)),
closes [#4093](#4093)
- **DqElement:** deprecate fromFrame function
([#4881](#4881))
([374c376](374c376)),
closes [#4093](#4093)
- **DqElement:** Truncate large `html` strings when the element has a
large outerHTML string
([#4796](#4796))
([404a4fb](404a4fb)),
closes [#4544](#4544)
- **get-xpath:** return proper relative selector for id
([#4846](#4846))
([1035f9e](1035f9e)),
closes [#4845](#4845)
- **i18n:** Add Portugal Portuguese translation
([#4725](#4725))
([5b6a65a](5b6a65a))
- incomplete with node on which an error occurred
([#4863](#4863))
([32ed8da](32ed8da))
- **locale:** Added ru locale
([#4565](#4565))
([067b01d](067b01d))
- **tap:** some best practice rules map to RGAA
([#4895](#4895))
([bc33f4c](bc33f4c))
- **td-headers-attr:** report headers attribute referencing other <td>
elements as unsupported
([#4589](#4589))
([ec7c6c8](ec7c6c8)),
closes [#3987](#3987)

### Bug Fixes

- **aria-allowed-role:** add form to allowed roles of form element
([#4588](#4588))
([8aa47ac](8aa47ac)),
closes
[/github.com/dequelabs/axe-core/blob/develop/lib/standards/html-elms.js#L264](https://github.com/dequelabs//github.com/dequelabs/axe-core/blob/develop/lib/standards/html-elms.js/issues/L264)
- **aria-allowed-role:** Add math to allowed roles for img element
([#4658](#4658))
([95b6c18](95b6c18)),
closes [#4657](#4657)
- **autocomplete-valid :** Ignore readonly autocomplete field
([#4721](#4721))
([491f4ec](491f4ec)),
closes [#4708](#4708)
- **autocomplete-valid:** treat values "xon" and "xoff" as
non-WCAG-violations
([#4878](#4878))
([52bc611](52bc611)),
closes [#4877](#4877)
- **axe.d.ts:** add typings for preload options object
([#4543](#4543))
([cfd2974](cfd2974))
- **button-name,input-button-name,input-img-alt:** allow label to give
accessible name
([#4607](#4607))
([a9710d7](a9710d7)),
closes [#4472](#4472)
[#3696](#3696)
[#3696](#3696)
- **captions:** fix grammar in captions check incomplete message
([#4661](#4661))
([11de515](11de515))
- **color-contrast:** do not run on elements with font-size: 0
([#4822](#4822))
([d77c885](d77c885)),
closes [#4820](#4820)
- consistently parse tabindex, following HTML 5 spec
([#4637](#4637))
([645a850](645a850)),
closes [#4632](#4632)
- **core:** measure perf for async checks
([#4609](#4609))
([7e9bacf](7e9bacf))
- fix grammar when using "alternative text" in a sentence
([#4811](#4811))
([237a586](237a586)),
closes [#4394](#4394)
- **get-ancestry:** add nth-child selector for multiple siblings of
shadow root ([#4606](#4606))
([1cdd6c3](1cdd6c3)),
closes [#4563](#4563)
- **get-ancestry:** don't error when there is no parent
([#4617](#4617))
([a005703](a005703))
- **locale:** fix typos in japanese (ja) locale
([#4856](#4856))
([3462cc5](3462cc5))
- **locale:** fixed typos in german (DE) locale
([#4631](#4631))
([b7736de](b7736de))
- **locale:** proofread and updated de.json
([#4643](#4643))
([8060ada](8060ada))
- **meta-viewport:** lower impact to moderate
([#4887](#4887))
([2f32aa5](2f32aa5)),
closes [#4714](#4714)
- **no-autoplay-audio:** don't timeout for preload=none media elements
([#4684](#4684))
([cdc871e](cdc871e))
- **performanceTimer:** throwing in axe catch clause
([#4852](#4852))
([a4ade04](a4ade04)),
closes
[/github.com/dequelabs/axe-core/blob/e7dae4ec48cbfef74de9f833fdcfb178c1002985/lib/core/base/rule.js#L297-L300](https://github.com/dequelabs//github.com/dequelabs/axe-core/blob/e7dae4ec48cbfef74de9f833fdcfb178c1002985/lib/core/base/rule.js/issues/L297-L300)
- **performanceTimer:** work in frames
([#4834](#4834))
([d7dfebc](d7dfebc))
- **rules:** Change "alternate text" to "alternative text"
([#4582](#4582))
([b03ada3](b03ada3))
- **target-size:** do not treat focusable tabpanels as targets
([#4702](#4702))
([60d11f2](60d11f2)),
closes [#4421](#4421)
[#4701](#4701)
- **type:** correct RuleError type
([#4893](#4893))
([d1aa8e2](d1aa8e2))
- **types:** correct raw types
([#4903](#4903))
([3eade11](3eade11))

This PR was opened by a robot 🤖 🎉
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants