Catching errors thrown from connectedCallback

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 (or AbortSignal 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.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.