ResizeObserver loop limit exceeded is the single most-reported console message tied to the ResizeObserver API, and it is almost always harmless — but the fix for it is worth doing properly rather than papering over.

What the Error Actually Means

The message is not a crash and not a thrown Error object your code can catch. It is a warning the browser's rendering engine emits when a ResizeObserver callback, in the course of running, causes the very element it is watching (or another element under the same observer) to change size again within the same rendering update. The specification caps how many times the engine will re-run the observation-and-notification cycle inside one frame; once that cap is hit, the remaining notifications for that frame are dropped and the browser logs the warning so you notice the loop exists.

This is covered at a mechanics level in ResizeObserver Mechanics & Triggers, which frames callback delivery as happening after layout but before paint — a scheduling window designed to be safe to read from, not necessarily safe to write into without care.

The feedback loop, step by step

  1. ResizeObserver delivers an entry for element A after layout settles.
  2. Your callback runs synchronously and sets A.style.height (or a class that changes height) based on the entry's dimensions.
  3. That style write invalidates layout again, inside the same rendering-update phase.
  4. Because A is still observed, the browser must re-check its box size before the frame can proceed.
  5. If the new size differs from what was last reported, a second notification is queued for the same observer in the same frame — the loop.

The diagram below shows the two paths: the broken synchronous-write path that trips the loop limit, and the deferred-write path that resolves cleanly on the next frame.

Synchronous write loop vs. rAF-deferred write in ResizeObserver callbacks Two vertical flows. The left flow shows observe, callback, synchronous style write, re-triggered observation, and the loop limit warning. The right flow shows observe, callback, entry read, requestAnimationFrame scheduling, and a deferred write on the next frame with no re-trigger. Synchronous write (broken) rAF-deferred write (fixed) observe() fires callback read entry.contentRect write style.height same rendering step size changed again loop limit exceeded notification dropped observe() fires callback read entry.contentRect requestAnimationFrame(fn) callback returns immediately next frame: write style observation settles no warning, no loop

Minimal Reproduction

This five-line snippet reliably triggers the warning in every current browser: a box whose observer writes back a size derived from its own last-read size, synchronously, inside the callback.

TypeScript
// TypeScript — reliably reproduces "ResizeObserver loop limit exceeded"
const box = document.createElement("div");
box.style.cssText = "width:100px;height:100px;background:#e8eef7;";
document.body.appendChild(box);

const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
  const [entry] = entries;
  const width = entry.contentRect.width;
  // BUG: synchronous write to the observed element, in the same callback
  box.style.width = `${width + 1}px`;
});

observer.observe(box);
JavaScript
// Plain JS equivalent
const box = document.createElement('div');
box.style.cssText = 'width:100px;height:100px;background:#e8eef7;';
document.body.appendChild(box);

const observer = new ResizeObserver((entries) => {
  const width = entries[0].contentRect.width;
  box.style.width = `${width + 1}px`; // triggers the loop
});

observer.observe(box);

Paste either version into a DevTools console on any page and watch the Console panel: within one or two frames you will see ResizeObserver loop limit exceeded (Chromium) or ResizeObserver loop completed with undelivered notifications (Firefox/Safari phrasing varies slightly, same underlying condition).

Why the Callback Mutating Layout Causes This

ResizeObserver callbacks are scheduled inside the browser's "update the rendering" steps — specifically after layout has been computed and before paint runs, as detailed in ResizeObserver Mechanics & Triggers. That timing is deliberately chosen so reading entry.contentRect or entry.borderBoxSize never forces a reflow. It says nothing, however, about writes. If your callback writes a layout-affecting property back onto an observed element, the engine must re-run layout to keep the render tree consistent — and since the element you just wrote to is still under observation, that re-run can produce a new entry for the same observer, in the same frame. The browser caps how many times it will chase that tail before giving up and logging the warning.

This is functionally identical to the general layout-thrashing pattern described in DOM Query Minimization — the difference is that ResizeObserver adds its own loop-detection guard rail specifically because this pattern is so common with resize-driven UI (auto-growing textareas, resizable panels, chart containers that report their own pixel size back to a canvas).

Real Fixes

Fix 1 — Defer the write to the next animation frame

The canonical fix. Reading inside the callback stays synchronous and safe; the write moves to requestAnimationFrame, which runs after the current rendering update has fully completed, breaking the same-frame cycle.

TypeScript
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
  const [entry] = entries;
  const width = entry.contentRect.width;

  requestAnimationFrame(() => {
    box.style.width = `${width + 1}px`; // now runs on the next frame — no loop
  });
});

Fix 2 — Write to a different element than the one you observe

If the callback only needs to reflect the observed element's size elsewhere (a canvas, a sibling, a CSS custom property on an ancestor), writing to a target that is not itself observed by the same observer avoids re-triggering it entirely, with no rAF deferral required:

TypeScript
interface ChartSurface {
  canvas: HTMLCanvasElement;
  container: HTMLElement;
}

function syncCanvasToContainer({ canvas, container }: ChartSurface): () => void {
  const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
    const [entry] = entries;
    const [size] = entry.contentBoxSize;
    // Safe: canvas is not the observed element, so this cannot re-trigger `observer`
    canvas.width = Math.round(size.inlineSize * devicePixelRatio);
    canvas.height = Math.round(size.blockSize * devicePixelRatio);
  });

  observer.observe(container);
  return () => observer.disconnect();
}

Fix 3 — Guard against redundant writes with a size comparison

Some loops only happen because the callback writes a value that is close to but not exactly the last-read value (subpixel rounding, devicePixelRatio scaling). Skip the write entirely when the delta is negligible:

TypeScript
let lastWidth = 0;
const EPSILON = 0.5;

const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
  const width = entries[0].contentRect.width;
  if (Math.abs(width - lastWidth) < EPSILON) return; // no meaningful change — skip write
  lastWidth = width;
  requestAnimationFrame(() => {
    box.style.setProperty("--observed-width", `${width}px`);
  });
});

Fix 4 — Break the loop explicitly with unobserve/re-observe

For cases where the element genuinely must be resized based on its own measurement (an auto-sizing widget), temporarily stop observing before writing, then resume:

TypeScript
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
  const [entry] = entries;
  const target = entry.target as HTMLElement;

  observer.unobserve(target);
  target.style.height = `${entry.contentRect.width / 2}px`;

  requestAnimationFrame(() => observer.observe(target));
});

observer.observe(box);

When it is safe to ignore or swallow the message

The warning is genuinely benign when it appears once or twice during a burst of rapid resizing (e.g., dragging a panel border) and then stops — the browser has recovered on its own. It is safe to filter out of error-reporting pipelines and CI test assertions:

TypeScript
// Filter this specific, non-fatal browser warning out of error monitoring
window.addEventListener("error", (event: ErrorEvent) => {
  if (event.message === "ResizeObserver loop limit exceeded") {
    event.stopImmediatePropagation();
  }
});

Older versions of webpack-dev-server and react-error-overlay listened on the same window error event and mistakenly treated this message as fatal, throwing up a full-screen red overlay in development. Current tooling versions already exclude this string; if you are on an older toolchain and cannot upgrade, the snippet above — scoped narrowly to the exact message text — is the accepted workaround. Do not silence all ResizeObserver-related console output this way, since a genuinely runaway loop (one that never recovers and pins the CPU) is a real bug worth seeing.

Verifying the Fix

  • Apply Fix 1 (or whichever fix matches your write pattern) to the reproduction snippet above.
  • Reload DevTools, clear the console, and re-run the modified snippet.
  • Resize the box element manually (or trigger the resize programmatically in a loop) and confirm no loop limit exceeded message appears.
  • Open the Performance panel, record a resize interaction, and confirm the ResizeObserver callback task duration is a single short block per frame — not a repeating cluster within one frame.

FAQ

Is 'ResizeObserver loop limit exceeded' a real error that breaks my app?

No. It is a recoverable warning, not a thrown exception. The browser skips the extra observation cycle for that frame and continues rendering normally. Your application keeps running; the only symptom is console noise or, in some dev tooling, a full-screen overlay.

Why does webpack-dev-server show this as a full-page error overlay?

Older versions of react-error-overlay and webpack-dev-server treated any window error event as fatal, and some browsers dispatch the ResizeObserver loop warning through that same event channel. The fix ships in current tooling versions, which explicitly ignore this message; if you are stuck on an old version, add a window.onerror filter that swallows only this exact string.

Does wrapping every ResizeObserver write in requestAnimationFrame fully prevent the loop error?

Yes, for the common case: a callback that reads entry dimensions and writes a style, class, or attribute back onto the observed element or an ancestor. Deferring that write to the next animation frame moves the mutation out of the current rendering-update step, so it cannot trigger a same-frame re-observation.

Can I just silence the error instead of fixing the underlying loop?

You can suppress the console message, but that only hides the symptom. If the callback is genuinely mutating the observed element's own size, the browser is still doing an extra observation-and-skip cycle every frame it happens, which wastes main-thread time. Fix the write pattern first; suppress only as a last resort for third-party code you cannot change.


↑ Back to ResizeObserver Mechanics & Triggers