# Fix: Hydration failed because the server rendered HTML didn't match the client

The server sent HTML, then React rendered the same component in the browser and got something different. React throws away that part of the page and renders it again on the client. The fix is to make the first browser render match the server: move anything that depends on the browser, the clock or random values into `useEffect`, and fix invalid HTML nesting.

| Fact | Detail |
|---|---|
| Error | `Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client.` |
| In production builds | `Minified React error #418` (React 19) · `#423` and `#425` (React 18) |
| Where you see it | Browser console, the Next.js error overlay in development |
| Usual cause | `window`, `localStorage`, `Date.now()` or `Math.random()` read during render, or a `<div>` inside a `<p>` |
| Usual fix | Render the same thing on both sides, then change it in `useEffect` |
| BugTape catches it | Yes, automatically, when the mismatch happens after BugTape starts |

## What the error means

Next.js renders your page on the server and sends finished HTML. In the browser, React runs the same components again and attaches event handlers to that HTML. This step is called hydration. React expects the browser's first render to produce exactly the same markup. When it does not, React reports error 418 and rebuilds the mismatched part of the tree on the client.

The page usually still works, which is why this error is easy to ignore. The cost is real: the user sees a flash as content changes, React does the work twice, and state in that part of the tree can reset.

React 18 printed the same problem as `Text content does not match server-rendered HTML.` (#425) or `There was an error while hydrating but React was able to recover by instead client rendering the entire root.` (#423).

## Common causes

1. **Browser-only values during render.** `typeof window !== 'undefined'`, `localStorage`, `navigator`, screen width or a cookie read in the browser give one answer on the server and another in the browser.
2. **Time and randomness.** `new Date()`, `Date.now()`, `Math.random()` or a random id differ between the two renders. Locale date formatting (`toLocaleDateString()`) differs when the server and the user are in different time zones or languages.
3. **Invalid HTML nesting.** A `<div>` inside a `<p>`, a `<p>` inside a `<p>`, an `<a>` inside an `<a>`, or a `<button>` inside a `<button>`. The browser repairs the HTML before React sees it, so the trees no longer match.
4. **Browser extensions.** Password managers, translators and ad blockers add attributes or elements to the page before React loads.
5. **Something between the server and the browser changes the HTML.** A CDN minifier or an injected script.
6. **iOS Safari turns phone numbers, dates and addresses into links.**

## How to fix it

### Read browser-only values in `useEffect`

Render a value that is the same on the server and in the browser, then update it after hydration.

```tsx
'use client';
import { useEffect, useState } from 'react';

export function Greeting() {
  const [name, setName] = useState<string | null>(null);

  useEffect(() => {
    setName(localStorage.getItem('name'));
  }, []);

  return <p>Hello{name ? `, ${name}` : ''}</p>;
}
```

Do not write `if (typeof window !== 'undefined')` inside the render body to choose different markup. That check is the cause, not the fix.

### Make dates and random values stable

Format dates on the server and pass the string down, or render them after mount. For ids, use `useId`, which gives the same value on both sides.

```tsx
'use client';
import { useEffect, useId, useState } from 'react';

export function LastSeen({ iso }: { iso: string }) {
  const id = useId();
  const [local, setLocal] = useState(iso.slice(0, 10)); // same on server and client

  useEffect(() => {
    setLocal(new Date(iso).toLocaleString());
  }, [iso]);

  return <time id={id} dateTime={iso}>{local}</time>;
}
```

### Fix invalid nesting

The error overlay in development names the element. The common case is a component that renders a `<div>` used inside a paragraph.

```tsx
// Before: <p> cannot contain <div>
<p>Status: <Badge /></p>        // Badge returns <div>…</div>

// After: use a <div> outside, or make Badge render a <span>
<div>Status: <Badge /></div>
```

### Skip server rendering for a component that only makes sense in the browser

A map, a chart that measures the window, or a widget that reads `window` at import time can be loaded only on the client.

```tsx
'use client';
import dynamic from 'next/dynamic';

const Map = dynamic(() => import('./map'), { ssr: false });
```

In the App Router, `ssr: false` works only inside a Client Component.

### Silence one element whose content must differ

For a single element, such as a timestamp, React accepts `suppressHydrationWarning`. It works one level deep. It hides the warning and React does not patch the text, so use it only where the difference is expected.

```tsx
<time dateTime={iso} suppressHydrationWarning>{new Date(iso).toLocaleTimeString()}</time>
```

### Stop iOS from rewriting text

```tsx
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />
```

### Check whether an extension causes it

Open the page in a private window with extensions off. If the error goes away, the cause is an extension. You cannot fix the extension. You can add `suppressHydrationWarning` to `<html>` or `<body>` when the extension changes only their attributes.

## How to catch it in production with BugTape

In production, React reports a hydration mismatch through `onRecoverableError`. Next.js passes the error to `reportError()` in modern browsers, which fires a window `error` event. The BugTape browser SDK listens for that event, so a mismatch is reported as **JavaScript error: Minified React error #418…** with the page URL, the stack and the steps the user took before it. Repeats on the same page become one issue with a count, so a mismatch that hits every visitor shows up as one issue that keeps growing.

The SDK records nothing before `init()` runs. If you load it with `next/script` and `strategy="afterInteractive"`, a mismatch during the very first hydration can happen before BugTape starts. A mismatch on a later client navigation, or inside a component that hydrates later (behind `Suspense` or `dynamic`), is caught. Test it on your own app: put `<p>{Math.random()}</p>` on a staging page, load it, and check that the issue arrives.

Add the browser SDK once in the root layout. The [Next.js recipe](/docs/examples/nextjs/README.md) has the full file:

```tsx
import Script from 'next/script';

export function BugTapeProvider() {
  return (
    <Script id="bugtape" type="module" strategy="afterInteractive">
      {`import { init } from 'https://app.bugtape.ai/bugtape.mjs';
        init({ apiKey: 'YOUR_PROJECT_KEY', release: '${process.env.VERCEL_GIT_COMMIT_SHA ?? ''}' });`}
    </Script>
  );
}
```

Set `release` so BugTape can tell you whether a fix held: 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

### Is a hydration error serious if the page still works?

It is worth fixing. React renders that part of the page a second time, the user can see content jump, and state in that subtree can reset. A mismatch caused by invalid nesting can also break event handlers in the repaired HTML.

### Why does the error only appear in production?

Development shows the full message and a diff in the overlay. Production builds print the short form, `Minified React error #418`, with a link to the full text. The cause is the same. Some mismatches only happen with production data, such as dates near midnight or a logged-in user's saved settings.

### Should I put suppressHydrationWarning on everything?

No. It works one level deep and stops React from fixing the text. Use it for one element whose content must differ, such as a local time.

### Does `use client` stop hydration errors?

No. Client Components are still rendered on the server first and then hydrated. Only `dynamic(..., { ssr: false })` skips the server render.

## Sources

- [Next.js: Text content does not match server-rendered HTML](https://nextjs.org/docs/messages/react-hydration-error), checked 2 Nov 2026.
- [React error codes](https://github.com/facebook/react/blob/main/scripts/error-codes/codes.json) (418, 423, 425), checked 2 Nov 2026.
- [Next.js `onRecoverableError`](https://github.com/vercel/next.js/blob/canary/packages/next/src/client/react-client-callbacks/on-recoverable-error.ts) and [React `reportGlobalError`](https://github.com/facebook/react/blob/main/packages/shared/reportGlobalError.js), checked 2 Nov 2026.

## Related errors

- [ReferenceError: window is not defined](/guides/window-is-not-defined/): `ReferenceError: window is not defined`
- [React: Objects are not valid as a React child](/guides/react-objects-not-valid-as-child/): `Objects are not valid as a React child`
- [ChunkLoadError: Loading chunk failed](/guides/chunkloaderror-loading-chunk-failed/): `ChunkLoadError: Loading chunk 123 failed.`
- [Next.js Server Components render error](/guides/nextjs-server-components-render-error/): `An error occurred in the Server Components render. The specific message is omitted in production builds`
- [React: Too many re-renders](/guides/react-too-many-re-renders/): `Too many re-renders. React limits the number of renders to prevent an infinite loop.`

All guides: [Fix an error](/guides/).

[Start free](/console/register?utm_source=bugtape_seo&utm_campaign=error-guides). Free plan · No credit card · Set up in minutes. Or [try the live demo](/demo/?from=landing&start=1#capture) first.
