Here’s a deep-in-the-weeds thing about web components that I ran into recently.
Let’s say you have a humble component:
class Hello extends HTMLElement {} customElements.define('hello-world', Hello);
And let’s say that this component throws an error in its connectedCallback
:
class Hello extends HTMLElement { connectedCallback() { throw new Error('haha!'); } }
Why would it do that? I dunno, maybe it needs to validate its props or something. Or maybe it’s just having a bad day.
In any case, you might wonder: how could you test this functionality? You might naïvely try a try
/catch
:
const element = document.createElement('hello-world'); try { document.body.appendChild(element); } catch (error) { console.log('Caught?', error); }
Unfortunately, this doesn’t work:
In the DevTools console, you’ll see:
Uncaught Error: haha!
Our elusive error is uncaught. So… how can you catch it? In the end, it’s fairly simple:
window.addEventListener('error', event => { console.log('Caught!', event.error); }); document.body.appendChild(element);
This will actually catch the error:
As it turns out, connectedCallback
errors bubble up directly to the window, rather than locally to where you called appendChild
. (Even though appendChild
is what caused connectedCallback
to fire in the first place. For the spec nerds out there, this is apparently called a custom element callback reaction.)
Our addEventListener
solution works, but it’s a little janky and error-prone. In short:
- You need to remember to call
event.preventDefault()
so that nobody else (like your persnickety test runner) catches the error and fails your tests. - You need to remember to call
removeEventListener
(orAbortSignal
if you’re fancy).
A full-featured utility might look like this:
function catchConnectedError(callback) { let error; const listener = event => { event.preventDefault(); error = event.error; }; window.addEventListener('error', listener); try { callback(); } finally { window.removeEventListener('error', listener); } return error; }
…which you could use like so:
const error = catchConnectedError(() => { document.body.appendChild(element); }); console.log('Caught!', error);
If this comes in handy for you, you might add it to your testing library of choice. For instance, here’s a variant I wrote recently for Jest.
Hope this quick tip was helpful, and keep connectin’ and errorin’!
Update: This is also true of any other “callback reactions” such as disconnectedCallback
, attributeChangedCallback
, form-associated custom element lifecycle callbacks, etc. I’ve just found that, most commonly, you want to catch errors from connectedCallback
.