jonelantha: Blog


Taming ResizeObserver errors

7th June 2020

Here we explore common ResizeObserver errors and how you can silence them

ResizeObserver is a welcome addition to the modern Web API and it's now supported in all major browers. If you're new to ResizeObserver be sure to take a look at ResizeObserver: it’s like document.onresize for elements.

We're here to look into a couple of cryptic ResizeObserver error messages that sometimes appear in error logs or the console:

  • ResizeObserver loop limit exceeded (Chrome)
  • ResizeObserver loop completed with undelivered notifications. (Safari, Firefox)

Although the W3C Resize Observer Spec does mention the error in some detail, you'd be forgiven if you got a bit lost reading the spec! That's ok, the document is aimed at browser implementors...

It turns out that the two errors mean pretty much the same thing, it's just that Chrome opted for slightly more helpful wording.

So what do the errors mean? Basically it's informing you that action was taken to stop a potentially infinite loop. This can occur if your ResizeObserver event handler makes DOM changes which result in a further size change to the element you're watching (we'll see a simple example of this below). If a second resize did happen while handling the first resize then ResizeObserver won't call your handler a second time, instead it generates the error.

The errors don't always appear in the console, but they're still there...

Most of the time the situation isn't really an error, just a notification. That's probably why Chrome and Firefox have decided not to report the errors in the console. However, the errors are still fired from the browser error handlers such as window.onerror or window.addEventListener('error', () => { ... }).

That means you may have the frustrating situation where you're seeing an error in front-end logs but you're not able to reproduce an error in the browser - if that's the case, try adding an error handler to make the errors more visible (we'll see a simple example below).

Time for a demo

On the example below the red box should stay square no matter how many animals are inside. Click Insert Next Animal - you should see an error appear in an alert.

(if you're using Safari you won't see the full error, just Script error - check the console to see the full error)

(to see the full source of the demo, 'View Frame Source' on the iframe above)

So what happened? We've setup a ResizeObserver to watch for size changes on the red box. The handler adjusts the red box's height to match its width - in other words it makes the red box square:

const resizeObserver = new ResizeObserver(() => {
  const { width } = redBox.getBoundingClientRect();
  
  redBox.style.height = `${width}px`;
});

resizeObserver.observe(redBox);

When Insert Next Animal was clicked the following happened:

  1. A new animal was added to the red box.
  2. The new animal caused the red box's width to grow (the red box is just a div with display: inline-block)
  3. The width change caused the ResizeObserver handler to fire.
  4. The handler set the red box's height to match the new width.
  5. ResizeObserver detected the second resize event (caused by the handler changing the height) but chose not to call the handler again in the current update frame (ResizeObserver does this to avoid a potentially infinite loop which could lead to an unresponsive browser). It raised an error instead which we saw in the alert.
  6. In the next update frame ResizeObserver calls the handler again. The handler sets the height again but because the red box is already a square it won't register as a size change - so no further events are fired.

Note: In order to see the hidden error in all browsers we've implemented the following very simple error handler:

window.onerror = message => {
  window.alert(`window.onerror: ${message}`);
};

Resizing can occur for many reasons

In the example above we triggered a second resize event by modifying the size of the element directly. It's worth noting that resize events can occur for other reasons too, for example:

  • making changes to the child DOM elements inside the element - resulting it in changing size.
  • changes to neighbouring or parent DOM elements which trigger a relayout - again this may result in the element changing size.

Great, so what can we do about it?

The errors are harmless but with a little bit of extra code you can get rid of them (which may make sense if the errors are spamming your front-end logs).

The solution is to suspend observation and restart on the next animation frame, but only if the element resized while the handler was executing.

Let's see what that looks like when applied to our square example:

const resizeObserver = new ResizeObserver(() => {
  const initialSize = redBox.getBoundingClientRect();
  const { width } = redBox.getBoundingClientRect();

  redBox.style.height = `${width}px`;

  const newSize = redBox.getBoundingClientRect();    if (    initialSize.width != newSize.width ||    initialSize.height != newSize.height  ) {    resizeObserver.unobserve(redBox);      window.requestAnimationFrame(() => {      resizeObserver.observe(redBox);    });  }});

We take a record of the size of the red box before we make any DOM changes and then compare it to the size afterwards. If it's changed then we tell ResizeObserver to stop observing the red box but resume observing on the next animation frame. This stops ResizeObserver from processing further size changes in the current frame - which means no error will be raised.

It's important to include the if statement as ResizeObserver always calls the handler as soon as you call observe (whether the element changed size or not). That means if we were to always call observe unconditionally in the handler then we'd end up with an infinite loop; that's why we should limit this behaviour to only run when we know we've changed the size.

Here's the earlier example with the fix applied. Click Insert Next Animal - notice the errors are gone!

One size to fit all

A single ResizeObserver can observe multiple elements. So our handler code should be able to detect changes to multiple elements. Let's extend our implementation and extract it into a helper function:

function execResizeCallbackWithSuspend(elements, resizeObserver, callback) {
  const initialSizes = elements.map(element => 
    element.getBoundingClientRect()
  );

  callback();

  const postCallbackSizes = elements.map(element =>
    element.getBoundingClientRect()
  );

  const resizedElements = elements.filter(
    (_, i) =>
      initialSizes[i].width !== postCallbackSizes[i].width ||
      initialSizes[i].height !== postCallbackSizes[i].height
  );

  resizedElements.forEach(element => resizeObserver.unobserve(element));

  window.requestAnimationFrame(() => {
    resizedElements.forEach(element => resizeObserver.observe(element));
  });
}

And here's how we'd use it with our red box example:

const resizeObserver = new ResizeObserver((entries) => {
  // derive the elements array from the `entries` argument
  const elements = entries.map(entry => entry.target);

  // alternatively we could just keep it simple with:
  // const elements = [redBox];

  execResizeCallbackWithSuspend(elements, resizeObserver, () => {
    const { width } = redBox.getBoundingClientRect();

    redBox.style.height = `${width}px`;
  });
});

Hopefully you found this useful and you'll no longer be exceeding your loop limit.

Happy ResizeObserver suspending!


© 2003-2020 jonelantha