-
Notifications
You must be signed in to change notification settings - Fork 333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add MissingElementError and use it within the Skip Link #4177
Conversation
📋 StatsFile sizes
Modules
View stats and visualisations on the review app Action run for e1eff14 |
Not a blocker in the slightest, but knowing what's missing is kinda useful! Could throw (and catch) an error in try {
this.$linkedElement = this.getLinkedElement()
} catch (cause) {
throw new MissingElementError('The linked HTML element does not exist', {
cause
})
} Would be helpful to see what |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approved! Added a comment about an error cause
For any error thrown it's good to explain what caused it
49b3faa
to
59b8117
Compare
@colinrotherham Added a commit that addresses your comment. As discussed, I've had to check the type of |
packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs
Outdated
Show resolved
Hide resolved
packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs
Outdated
Show resolved
Hide resolved
packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs
Outdated
Show resolved
Hide resolved
59b8117
to
505f7df
Compare
505f7df
to
0fc25bc
Compare
export class MissingElementError extends GOVUKFrontendError { | ||
name = 'MissingElementError' | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a blocker, but it's triggered a discussion
To keep file size down, could we use Error { cause }
to keep our custom errors to a minimum? Otherwise our published package will grow as custom errors are added
E.g. Using TypeError "…not of the expected type" for null
or missing things
Component errors
Here's an example with nested { cause }
to make the error more informative:
throw new ComponentError('I has died', {
cause: new TypeError("Argh, 'ere be no link target #pirates"),
component: 'Skip link'
}
But using the nested { cause }
approach to surface all that useful information. It's handy for monitoring tools like Sentry which links chained errors + stack traces
For extra user friendliness a custom toString()
(see below) could log the component name:
ComponentError: "Skip link: I has died"
→ Cause: TypeError: "Argh, 'ere be no link target #pirates"
export class MissingElementError extends GOVUKFrontendError { | |
name = 'MissingElementError' | |
} | |
export class ComponentError extends GOVUKFrontendError { | |
name = 'ComponentError' | |
component = 'Component name' | |
toString() { | |
return `${this.component}: ${this.message}`; | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's also a gotcha
We'd need to confirm Babel transforms or a polyfill for JavaScript built-in: Error: cause
0fc25bc
to
c61fb5a
Compare
throw new MissingElementError( | ||
'Skip link: the linked HTML element does not exist', | ||
{ | ||
cause: cause instanceof Error ? cause : undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For JavaScript built-in: Error: cause browser support are we happy treating this as optional?
If so, do you mind adding a bit of safety to the GOVUKFrontendError
constructor?
i.e. Use feature detection, not a polyfill
export class GOVUKFrontendError extends Error {
name = 'GOVUKFrontendError'
/**
* @param {string} message
* @param {ErrorOptions} options
*/
constructor(message, { cause, ...options }) {
super(message, options)
// Add cause property where supported
if (cause && new Error('test', { cause }).cause === cause) {
this.cause = cause
}
}
}
We'll need to:
- ESLint ignore "ES2022 Error Cause is forbidden" for feature detection purposes
- ESLint ignore "ES2018 rest/spread properties are forbidden" if Babel handles it already
- Triple check my feature detection idea, maybe it needs a try/catch?
For 3) it's mainly because 'cause' in Error.prototype
didn't work 🙈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or we remove { cause }
for another day?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm leaning toward leaving { cause }
for now - feels like p'raps we need a bit more thinking about how we make it work and can do that as part of iterating on these errors.
I've pushed a change to use the error messages to convey the error to the user, whaddya reckon?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very clever, absolutely love it
packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs
Outdated
Show resolved
Hide resolved
92b7643
to
a64e54a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Brilliant @domoscargin
Some non-blocking things since the last push but approved! Thanks again for making the errors actionable, always good to explain why an error was throw if we can
Appreciate we've used double quotes here but back ticks in #4104 so I've written up some content feedback for later:
Error causes
We had some good chats about error causes, especially with monitoring tools like Sentry linking chained errors + stack traces together for inspection
So we should pull out some actions/discussions such as:
- Using Error
cause
to reduce custom errors (see comment) - Spike Error
cause
feature detection (see comment)
const linkedElement = document.getElementById(linkedElementId) | ||
|
||
if (!linkedElement) { | ||
throw new TypeError(`Target selector "#${linkedElementId}" not found`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a blocker, but we now say "linked element" in the fallback error but "target selector" here
throw new TypeError(`Target selector "#${linkedElementId}" not found`) | |
throw new TypeError(`Linked element selector "#${linkedElementId}" not found`) |
try { | ||
const $linkedElement = this.getLinkedElement() | ||
this.$linkedElement = $linkedElement | ||
} catch (err) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another non-blocker, but we use error
everywhere else
} catch (err) { | |
} catch (error) { |
a64e54a
to
e44f67f
Compare
e44f67f
to
e1eff14
Compare
Closes #4128
Throws an error instead of returning if the linked element is missing.
I considered implementing a separate method to check for this, but that feels a bit needless for Skip Link - it may be necessary for more complex components, though.
There's also an argument for throwing errors within
getLinkedElement
andgetFragmentFromUrl
(our error message could potentially be more helpful if we could provide thelinkedElementId
, for example), but since these get caught by the existing check, I kept things simple.