Fix this error · React and Next.js
Fix: Maximum update depth exceeded
Maximum update depth exceeded (#185) comes from setState loops in effects, componentDidUpdate or store selectors. Causes, fixes and production capture.
A component sets state after every commit, so each update causes another one. React stops after 50 nested updates and throws. The usual cause is a setState in componentDidUpdate or useLayoutEffect with no condition, or an effect whose dependency is a new object on every render. The fix is to update only when a value really changed, or to stop storing a value you can compute.
| Fact | Detail |
|---|---|
| Error | Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. |
| In production builds | Minified React error #185 |
| Development warning | Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render. |
| Where you see it | Browser console, the Next.js error overlay |
| Usual cause | State set in componentDidUpdate, useLayoutEffect or through a store selector on every update |
| Usual fix | Compare before setting state, keep dependencies stable, or compute the value during render |
| BugTape catches it | Yes, automatically, for the thrown error (#185) when no error boundary of yours catches it. The useEffect warning only exists in development |
What the error means
React counts synchronous updates that are scheduled while it commits the previous one. In the React source the limit is 50 (NESTED_UPDATE_LIMIT). When a component keeps scheduling an update from componentDidUpdate, useLayoutEffect or a store subscription, the count passes the limit and React throws error #185.
There are two versions of the message. The thrown error names componentWillUpdate and componentDidUpdate, but it also covers useLayoutEffect and other synchronous updates. The useEffect version is a separate check that React runs only in development and prints with console.error. React does not throw in that case. A production build keeps re-rendering, which shows up as a slow tab, a hot CPU or the same request sent again and again.
This is a different error from Too many re-renders (#301), which means state was set during render itself.
Common causes
- An effect dependency that changes on every render. An object, array or function created in the component body is listed in the dependency array, and the effect sets state.
- An effect or layout effect with no dependency array that sets state. It runs after every render, so every render sets state again.
setStateincomponentDidUpdatewithout a comparison. Class components update, then update again.- A store selector that returns a new object. Zustand v5 and other stores built on
useSyncExternalStorecompare results withObject.is. A selector such as(s) => ({ a: s.a, b: s.b })returns a new object each time. React printsThe result of getSnapshot should be cached to avoid an infinite loopin development before the error.
How to fix it
Make effect dependencies stable
Move constant objects out of the component, or depend on the primitive values inside them.
// Before: options is a new object each render, so the effect runs each render
function Results({ query }: { query: string }) {
const [rows, setRows] = useState<Row[]>([]);
const options = { query, limit: 20 };
useEffect(() => {
search(options).then(setRows);
}, [options]);
// ...
}
// After: depend on the value that changes
function Results({ query }: { query: string }) {
const [rows, setRows] = useState<Row[]>([]);
useEffect(() => {
search({ query, limit: 20 }).then(setRows);
}, [query]);
// ...
}If the dependency must be an object or function, create it with useMemo or useCallback so it keeps the same identity between renders. The react-hooks/exhaustive-deps lint rule points out dependencies that change on every render.
Compute the value instead of syncing it with an effect
When an effect only copies props or state into other state, remove both the effect and the extra state.
// Before
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${first} ${last}`);
});
// After
const fullName = `${first} ${last}`;Compare before setting state in componentDidUpdate
componentDidUpdate(prevProps: Props) {
// Before: this.setState({ value: this.props.value }) on every update
if (prevProps.value !== this.props.value) {
this.setState({ value: this.props.value });
}
}The same rule applies to useLayoutEffect: give it a dependency array, and set state only when the measured value differs from the current one.
useLayoutEffect(() => {
const next = ref.current?.offsetHeight ?? 0;
setHeight((prev) => (prev === next ? prev : next));
}, [items]);Returning the previous value lets React skip the update.
Return stable values from store selectors
In Zustand v5, wrap a selector that builds an object with useShallow, or select each value on its own.
import { useShallow } from 'zustand/react/shallow';
const { searchValue, setSearchValue } = useStore(
useShallow((state) => ({
searchValue: state.searchValue,
setSearchValue: state.setSearchValue,
})),
);
// Or
const searchValue = useStore((state) => state.searchValue);For your own useSyncExternalStore call, getSnapshot must return the same value until the store changes.
How to catch it in production with BugTape
In production React throws Minified React error #185 with a link to the full text. Where the error goes depends on your error boundaries.
- No error boundary of yours catches it. React 19 passes it to
reportError(), which fires a windowerrorevent, and React 18 rethrows it as an uncaught error. The BugTape browser SDK reports it automatically with the page URL, the stack and the steps the user took before it. In the Next.js App Router, an error that only the built-in boundary catches also goes toreportError(). - Your error boundary catches it, including a Next.js
error.tsx. React and Next.js then only callconsole.error. BugTape records console output into a report but does not start a report from it. Add one line to the boundary:
'use client';
import { useEffect } from 'react';
export default function Error({ error, retry }: { error: unknown; retry: () => void }) {
useEffect(() => {
reportError(error); // fires the window error event BugTape listens for
}, [error]);
return <button onClick={() => retry()}>Try again</button>;
}The useEffect version of the message is a development-only console warning, so there is nothing for BugTape to catch in production. If you suspect that loop in production, look for a page where the same request repeats. BugTape reports a failed request only when it follows a click within 8 seconds, so a background loop of successful requests does not start a report. Fix it in development, where React prints the warning and the component stack.
The SDK records nothing before init() runs. Add it once (see the quickstart):
import { init } from '@bugtape/sdk';
init({ apiKey: 'YOUR_PROJECT_KEY', release: 'YOUR_GIT_SHA' });Repeats of #185 from the same place group into one issue with a count. Set release so that when you mark the issue fixed, a report from a newer release reopens it as a regression. Your coding agent can read the issue over MCP (npx -y bugtape connect claude, or cursor or codex) and propose the change. You review the pull request.
FAQ
What is React error 185?
It is the production code for Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. The production message links to react.dev/errors/185. Run the app in development to see the full message and which component is updating.
Why do I get this error with useEffect if React says it only warns?
The thrown error covers synchronous updates. A useEffect loop alone gives the development warning. When the loop also involves a synchronous update, such as a useLayoutEffect or a store built on useSyncExternalStore, React throws #185. Look at every effect and subscription in the component stack the development message shows.
How do I find which component causes it?
Run the app in development. The console error includes the component stack, and React DevTools can highlight components as they render. Temporarily logging each dependency of the suspect effect shows which one changes on every render.
Is this the same as "Too many re-renders"?
No. "Too many re-renders" (#301) means a state setter ran in the component body during render. "Maximum update depth exceeded" means updates kept coming after each commit. The first is usually an onClick={handler()} mistake. The second is usually an effect, lifecycle method or subscription.
Sources
- React error codes (185), checked 2 Nov 2026.
- React source, ReactFiberWorkLoop.js (
NESTED_UPDATE_LIMIT = 50, the development-onlyuseEffectwarning), checked 2 Nov 2026. - React source, ReactFiberHooks.js (
getSnapshotwarning), checked 2 Nov 2026. - React docs: useEffect, "My Effect keeps re-running in an infinite cycle", checked 2 Nov 2026.
- Zustand docs: useShallow, "Maximum update depth exceeded", checked 2 Nov 2026.
Related errors
- React: Too many re-renders:
Too many re-renders. React limits the number of renders to prevent an infinite loop. - React: Invalid hook call:
Invalid hook call. Hooks can only be called inside of the body of a function component. - Next.js hydration mismatch:
Hydration failed because the server rendered HTML didn't match the client - Next.js Server Components render error:
An error occurred in the Server Components render. The specific message is omitted in production builds - React: Objects are not valid as a React child:
Objects are not valid as a React child
All guides: Fix an error.
Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.