Is it okay to make connectedCallback async?

One question I see a lot about web components is whether this is okay:

async connectedCallback() {
  const response = await fetch('/data.json')
  const data = await response.json()
  this._data = data
}

The answer is: yes. It’s fine. Go ahead and make your connectedCallbacks async. Thanks for reading.

What? You want a longer answer? Most people would have tabbed over to Reddit by now, but sure, no problem.

The important thing to remember here is that an async function is just a function that returns a Promise. So the above is equivalent to:

connectedCallback() {
  return fetch('/data.json')
    .then(response => response.json())
    .then(data => {
      this._data = data
    })
}

Note, though, that the browser expects connectedCallback to return undefined (or void for you TypeScript-heads). The same goes for disconnectedCallback, attributeChangedCallback, and friends. So the browser is just going to ignore whatever you return from those functions.

The best argument against using this pattern is that it kinda-sorta looks like the browser is going to treat your async function differently. Which it definitely won’t.

For example, let’s say you have some setup/teardown logic:

async connectedCallback() {
  await doSomething()
  window.addEventListener('resize', this._onResize)
}

async disconnectedCallback() {
  await undoSomething()
  window.removeEventListener('resize', this._onResize)
}

You might naïvely think that the browser is going to run these callbacks in order, e.g. if the component is quickly connected and disconnected:

div.appendChild(component)
div.removeChild(component)

However, it will actually run the callbacks synchronously, with any async operations as a side effect. So in the above example, the awaits might resolve in a random order, causing a memory leak due to the dangling resize listener.

So as an alternative, you might want to avoid async connectedCallback just to make it crystal-clear that you’re using side effects:

connectedCallback() {
  (async () => {
      const response = await fetch('/data.json')
      const data = await response.json()
      this._data = data
  })()
}

To me, though, this is ugly enough that I’ll just stick with async connectedCallback. And if I really need my async functions to execute sequentially, I might use the sequential Promise pattern or something.

One response to this post.

  1. dritter03's avatar

    Posted by dritter03 on October 4, 2024 at 4:15 AM

    Hey!

    Thanks for the great article!

    IMHO I don’t think the sequential Promise pattern really solves the problem, because we have no control over the connecteCallback itself.

    In my case I had an custom function on my Component that does some DOM manipulation in response to a fetch in the connectedCallback. That custom function was called just after the component was inserted into the DOM, so naturally the fetch took longer than executing the custom function, hence the DOM manipulation was never visible in the custom function (the browser never waited for connectedCallback to finish).

    My solution is to create a custom Promise and resolve that on the end of connectCallback and await that in my custom function.

    Pseudo code:

    class MyComponent extends HTMLElement {

    #myElement

    #connectedResolver = null#connected = new Promise((resolve) => { this.#connectedResolver = resolve})

    async connectedCallback () {

    await fetch(‘https://example.com’)

    this.#myElement = document.createElement(‘h1’)

    this.#myElement.innerText = ‘Headline (should be something from the fetch response)’

    this.appendChild(this.#myElement)

    this.#connectedResolver()

    }

    async customFunction() {

    await this.#connected

    // Do something with myElement

    this.#myElement.style.color = ‘red’ // <- Without the promise, this.#myElement is null

    }

    <script>

    // Call the customFunction immediately after the component was inserted into the DOM

    const body = document.querySelector(‘body’)

    const myComponent = document.createElement(‘my-component’)

    body.appendChild(myComponent)

    myComponent.customFunction()

    </script>

    Thanks again!

    Reply

Leave a reply to dritter03 Cancel reply

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