TL;DR / ResizeObserver detects element size changes asynchronously, but resizing elements inside the callback can trigger infinite observation loops that the browser breaks by throwing an error event.
How It Works
┌──────────┐ ┌────────────────┐
│ Element │─────────→│ ResizeObserver │
└──────────┘ └────────────────┘
│
┌─────┘
↓
┌─────────────────┐
│ Callback fires │
└─────────────────┘
│
│
↓
┌────────────────────┐
│ Resize element │ infinite
│ in callback │ loop!
└────────────────────┘
│
│
↓
┌────────────────────┐
│ Observer fires │
│ again... │
└────────────────────┘
│
│
↓
┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ LOOP LIMIT HIT ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛
ResizeObserver fills a gap that CSS and other DOM APIs cannot: knowing when an individual element's dimensions change, regardless of what caused the change. Window resize events only fire when the viewport changes. Media queries react to viewport dimensions. Neither tells you when a specific <div> grew by 20 pixels because its text content changed or a sibling was removed.
Observation Timing
ResizeObserver callbacks run after layout but before paint, at a specific point in the browser's rendering pipeline. The spec defines this precisely: after style and layout calculations, the browser gathers all pending resize observations and delivers them. Each ResizeObserverEntry contains the target element, its contentRect (a DOMRectReadOnly of the content box), borderBoxSize, and contentBoxSize as arrays of ResizeObserverSize objects with inlineSize and blockSize properties.
The contentRect property is a legacy convenience -- it reports the content box dimensions as width, height, x, and y. For modern usage, prefer contentBoxSize or borderBoxSize since these correctly handle writing modes (the inlineSize/blockSize distinction matters for vertical text).
The Infinite Loop Problem
The core design challenge with ResizeObserver is that its callback runs during the rendering pipeline, and code inside that callback can change element sizes, which would require another round of observation. This creates a potential infinite loop: observe size change, react by changing sizes, observe that change, react again, endlessly.
The spec solves this with a depth-based loop limit. Every element in the DOM has an implicit "depth" (its distance from the root). After processing resize observations, the browser checks if any newly resized elements have a depth greater than or equal to the shallowest depth observed in this round. If so, it gathers and delivers those observations in another round. This continues, but only for elements deeper in the tree than the previous round's shallowest target. The loop terminates when no new observations qualify or when a maximum iteration count is reached.
When the loop limit is hit, the browser fires an ErrorEvent on the window with the message "ResizeObserver loop completed with undelivered notifications." This is not a thrown exception -- it is an error event. The undelivered observations are simply skipped for that frame and delivered on the next frame instead.
Why the Loop Limit Gets Hit
The most common trigger is resizing a parent or sibling element inside a ResizeObserver callback. If element A observes its children and adjusts its own height, and element B observes A and adjusts something that affects A's children, you get a cross-depth feedback loop. Single-element self-adjustment (like setting a child's size based on the observed parent's size) is typically safe because it only goes deeper in the tree.
Layout libraries, virtualized lists, and chart components frequently hit this. Any component that measures its container and then adjusts its own dimensions based on the measurement is a candidate for the loop error. The error is benign in the sense that the browser continues rendering, but the skipped frame of observations means visual glitches can appear as elements settle into their final sizes one frame late.
Practical Patterns
For responsive components, observe the container and read contentBoxSize[0].inlineSize to get the available width in the element's writing mode. Avoid writing back to the same element you are observing -- instead, write to children or use CSS custom properties that children reference. If you must create a feedback loop, use requestAnimationFrame to defer the size change to the next frame, breaking the synchronous cycle at the cost of a one-frame delay.
Disconnecting observers when components unmount is essential. ResizeObserver holds strong references to observed elements. Failing to call observer.disconnect() or observer.unobserve(element) when a component is destroyed means the observer continues processing that element on every layout pass, even if the component's callback closure is stale.
Gotchas
- The loop limit error is an
ErrorEventonwindow, not a thrown exception -- it will not be caught by try/catch. You needwindow.addEventListener('error', ...)to detect it, and most error monitoring tools flag it as noise. - Observing and resizing the same element in the callback creates a direct feedback loop -- defer writes with
requestAnimationFrameor write to descendant elements instead. contentRectdoes not include padding or border -- it is the content box only. For full element dimensions, useborderBoxSize.- ResizeObserver does not fire for elements with
display: none-- zero-size elements are not observed until they gain dimensions. Toggling display can cause unexpected observation gaps. - Multiple observers on the same element compound the loop problem -- each observer independently participates in the depth-based loop check, making convergence harder.