What "ResizeObserver Loop Completed" Errors Mean
Understand ResizeObserver loop errors, why the browser defers notifications, how to spot real layout loops, and safely filter both error messages in trackers and Cypress.
“ResizeObserver loop completed with undelivered notifications” is a browser error event, not an exception from your code. It means some resize notifications missed the current frame and were pushed to the next one, and in most cases you can safely ignore it.
It tends to show up in an error tracker as hundreds of events with no useful stack, or it fails a Cypress run, and often there’s no ResizeObserver anywhere in your own code. This article explains what the browser is doing, shows a minimal repro, and sets out how to tell noise from a real loop. It ends with a filter that matches both versions of the message.
Key Takeaways
- The browser fires “ResizeObserver loop completed with undelivered notifications.” as an error event on
windowwhen resize notifications have to wait until the next frame. - Older browser versions reported the same condition as “ResizeObserver loop limit exceeded”, so any filter or test handler needs to match both messages.
- The safeguard stops the browser from locking up. It doesn’t stop the loop: a callback that keeps resizing the element it observes fires the error on every frame.
try/catchnever sees the error, butwindow.onerror, error-tracking SDKs and Cypress’suncaught:exceptionhandler all do.- Filter the error in your tracker, not with a global
window.onerrorswallow, and keep watching how often it fires.
What Does “ResizeObserver Loop Completed With Undelivered Notifications” Mean?
The “ResizeObserver loop completed with undelivered notifications” message means the browser had resize notifications it couldn’t deliver within one frame, so it deferred them. If you need a refresher on the API itself, see ResizeObserver vs window resize.
The browser handles resize observations after layout and before paint. If a callback changes layout, the browser recalculates and runs callbacks again. On each pass it only handles elements deeper in the DOM than the shallowest element handled on the previous pass. Anything that doesn’t meet that rule waits for the next frame, and the browser reports the error. MDN’s Observation Errors section describes this sequence, and the Resize Observer specification defines the depth rule.
You’ll see one of two messages:
ResizeObserver loop completed with undelivered notifications.(the current wording, with a trailing period)ResizeObserver loop limit exceeded(the wording used by older browser versions)
A Minimal Repro
This file reproduces both behaviours. The callback changes the width of the element it is observing.
<div id="box" style="width:100px;height:40px;background:tomato"></div>
<script>
const box = document.getElementById('box');
const LOOP = false; // set to true for the endless case
window.addEventListener('error', (e) => console.log('window error:', e.message));
const ro = new ResizeObserver(([entry]) => {
const width = entry.contentBoxSize[0].inlineSize;
entry.target.style.width = LOOP ? `${width + 10}px` : '200px';
});
ro.observe(box);
</script>
With LOOP = false, the first callback resizes the box to 200px. That resize is on the same element, so it isn’t deeper than the element just handled. The browser defers it and logs the error once. On the next frame the callback writes 200px again, nothing changes, and the observer stops firing.
With LOOP = true, every frame makes the box wider, and the error is logged on every frame.
Why Doesn’t try/catch Catch the ResizeObserver Error?
try/catch never catches the ResizeObserver loop error because your callback doesn’t throw anything. It finishes normally. The browser dispatches an error event on window afterwards, outside any call stack you control.
try {
ro.observe(box);
} catch (err) {
console.log('never reached'); // the error is dispatched on window later
}
window.onerror and error listeners catch window-level error events, and error-tracking SDKs hook into the same events, so trackers record it. The event carries no Error object with a stack trace, so trackers usually show no useful stack.
When Is the ResizeObserver Error a Real Problem?
The ResizeObserver loop error matters when the layout doesn’t settle. MDN points out that the safeguard prevents a browser lock-up but not the loop itself. Even a loop that does settle can briefly show an intermediate layout while the changes are spread across frames.
| What you see | What it means | What to do |
|---|---|---|
| A few events after load or a layout change, and the page looks stable | The loop settled within a frame or two | Filter it |
| Bursts tied to one component, with a visible jump | A component is resizing the box it observes | Fix it or report it upstream |
| A continuous stream, a growing element, or rising CPU | A loop that never settles | Fix it now |
How Do You Fix a ResizeObserver Loop?
Fix a ResizeObserver loop by stopping the callback from resizing the element it observes, or by moving that write out of the current frame. Work through these options in order.
- Don’t resize the observed element from its own callback. Write to an element whose size doesn’t feed back into the observation. For purely visual responsiveness, CSS container queries avoid JavaScript altogether.
- Defer with
requestAnimationFrame. Moving the write intorequestAnimationFrameruns it in the next frame, so the error stops. MDN recommends this when you want continuous resizing. In theLOOPcase the box still grows forever. Only the error goes away. - Skip sizes you set yourself. For loops you didn’t intend, MDN suggests recording the size you expect for each element in a
WeakMap:
const expected = new WeakMap();
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentBoxSize[0].inlineSize;
if (expected.get(entry.target) === width) continue; // our own change
requestAnimationFrame(() => {
const next = width + 10;
entry.target.style.width = `${next}px`;
expected.set(entry.target, next);
});
}
});
Because state is stored per element, observing several elements is safe. When the callback sees the width it set itself, it skips the entry and the loop ends.
UI Libraries Trigger It Too
The usual trigger is a ResizeObserver callback that changes the size of the element it observes. That callback often lives in a component library rather than in your own code. Components that measure their container and then resize themselves or a child are the typical source, for example auto-sizing text areas, virtualised tables, charts and popovers. If the error appears when a particular component mounts or opens, update the library or report a reproduction upstream.
How Do You Filter the Error Safely?
Match both messages with one pattern, and apply it in your tracker and test runner, not in a global handler.
const RO_LOOP_ERROR =
/ResizeObserver loop (completed with undelivered notifications|limit exceeded)/;
// cypress/support/e2e.js
Cypress.on('uncaught:exception', (err) => {
if (RO_LOOP_ERROR.test(err.message)) return false;
});
Cypress raises uncaught:exception whenever your app throws an error that nothing catches, and a handler that returns false tells Cypress not to fail the test. The Cypress Catalog of Events documents this. In Sentry, add ignoreErrors: [RO_LOOP_ERROR]. According to the Sentry options reference, string entries match substrings and regex entries are tested as written. Recent Sentry JavaScript SDKs already ignore the modern wording by default but not the legacy one, so a custom entry is only needed for the old string or for older SDKs.
Don’t swallow the error in window.onerror. A blanket suppression hides the endless-loop case too, and you lose the frequency signal. Filtering at the tracker level, as described in these error logging best practices, keeps that decision visible and easy to reverse. Then watch the count. An occasional burst after a layout change is noise. A steady stream from one session points to a loop that never settles. A session replay shows whether the user saw a layout jump at the moment the error fired, which tells you whether it’s noise. The same triage applies to other noisy production errors such as “Failed to Fetch Dynamically Imported Module”.
Conclusion
In most cases, a “ResizeObserver loop completed” error means the browser deferred some notifications and the layout settled a frame later. Check whether the layout settles and whether anything visibly jumps. If both answers are good, filter both messages in your tracker. If not, find the callback that resizes the element it observes and fix it with a deferral or an expected-size guard.
FAQs
Is setTimeout or debounce a better fix than requestAnimationFrame?
No. Both move the resize write out of the current frame, so the error stops, but neither stops a loop that keeps growing the element. A setTimeout callback can run after the browser has already painted, so users may see the old layout for a frame before the change lands. requestAnimationFrame runs before the next paint, which keeps the update in step with rendering.
Why does the error appear right after a component mounts?
ResizeObserver sends a first notification when observe() is called on an element that is rendered and has a non-zero size. If the callback writes a new size to that element on this first notification, the browser defers the follow-up notification and reports the error straight away. Components that measure and resize themselves on mount trigger it this way even when nothing on the page is resized.
Can CSS container queries replace a ResizeObserver callback?
Yes, when the callback only changes styles based on a container's width. Container size queries apply size containment, so the container does not take its size from its contents and styling cannot feed back into the measurement. Size queries work in Chrome 105, Safari 16, Firefox 110 and later. Keep ResizeObserver for work CSS cannot do, such as recalculating chart dimensions or virtualised row counts.
How do I find which code triggers the error when the stack trace is empty?
In development, temporarily replace window.ResizeObserver with a subclass that logs each observed element and wraps the callback. Then reproduce the error and check which element was resized just before the window error event. If the callback lives inside a third-party library, removing components one at a time until the error stops narrows down the source.