Playwright can catch Next.js hydration errors by listening for browser console failures and then exercising an interaction that only works after React has attached its event handlers. This detects pages that return 200 OK and display server-rendered content but fail when a customer clicks, types, or navigates.
Next.js defines hydration as React attaching event handlers to the server-rendered HTML. Its hydration error documentation lists mismatches between the server and first browser render as the underlying problem.
Key takeaways
- Attach the console listener before
page.goto()so early hydration errors are not missed. - Test a real client-side interaction in addition to inspecting console output.
- Filter known third-party console noise rather than failing on every error.
- Run Playwright against a production build in CI and against the deployed application on a schedule.
- Capture the route, console message, and trace when the check fails.
What is a Next.js hydration error?
Next.js renders HTML on the server, sends it to the browser, and lets React hydrate that markup into an interactive application. The server HTML and the browser's first React render need to agree.
A mismatch can produce a console error, cause React to replace part of the tree, or leave an interaction in the wrong state. The user may see the page while a menu, form, or client-side transition fails.
Next.js documents several common causes:
- Invalid nesting such as a
<div>inside a<p>. - Rendering different content when
windowexists. - Reading browser-only APIs such as
localStorageduring rendering. - Using time-dependent values such as
Date()in the initial output. - CSS-in-JS configuration that produces different markup.
- Browser extensions or CDN transformations that modify the HTML.
These are browser-runtime failures, which is why an HTTP uptime check cannot identify them.
Capture hydration messages with Playwright
Create the console listener before navigation and store relevant errors:
import { test, expect } from '@playwright/test'
test('Next.js route hydrates and remains interactive', async ({ page }) => {
const hydrationErrors = []
page.on('console', message => {
if (message.type() !== 'error') return
const text = message.text()
if (/hydration|did not match|server-rendered html|text content/i.test(text)) {
hydrationErrors.push(text)
}
})
await page.goto(process.env.NEXTJS_MONITOR_URL, {
waitUntil: 'domcontentloaded',
})
await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
expect(hydrationErrors, hydrationErrors.join('\n')).toEqual([])
})The regular expression covers common React and Next.js wording without depending on one exact framework version. Keep the raw messages in the assertion so the failed run explains what React reported.
Verify behavior after hydration
Console output alone is incomplete. Some mismatches are recovered by React, while other client failures do not use the word hydration.
Choose a stable interaction on the monitored route:
await page.getByRole('button', { name: /open menu/i }).click()
await expect(page.getByRole('navigation')).toBeVisible()
await page.getByRole('link', { name: /pricing/i }).click()
await expect(page).toHaveURL(/\/pricing/)
await expect(page.getByRole('heading', { name: /pricing/i })).toBeVisible()This confirms that React event handlers and client-side routing work. Pick an interaction that does not create data or depend on a volatile campaign.
For a form-heavy route, enter a harmless value and verify client validation. The forms and inputs guide provides stable patterns for those controls.
Record page and request failures too
A failed chunk or API request can leave a page non-interactive without producing the expected hydration text. Capture page exceptions and failed first-party requests:
const runtimeErrors = []
const failedRequests = []
page.on('pageerror', error => runtimeErrors.push(error.message))
page.on('requestfailed', request => {
const url = new URL(request.url())
if (url.origin === process.env.NEXTJS_APP_ORIGIN) {
failedRequests.push(`${request.method()} ${url.pathname}`)
}
})
await page.goto(process.env.NEXTJS_MONITOR_URL)
expect(runtimeErrors, runtimeErrors.join('\n')).toEqual([])
expect(failedRequests, failedRequests.join('\n')).toEqual([])Filter first-party requests so an optional analytics call does not open an outage. If a third-party script is critical to checkout or authentication, add its host explicitly.
Compare server HTML with the hydrated page
When diagnosing a mismatch, run two focused checks. One browser context loads the page with JavaScript disabled, and the other loads it normally:
test('server and hydrated headings agree', async ({ browser }) => {
const serverContext = await browser.newContext({ javaScriptEnabled: false })
const serverPage = await serverContext.newPage()
await serverPage.goto(process.env.NEXTJS_MONITOR_URL)
const serverHeading = await serverPage.getByRole('heading', { level: 1 }).textContent()
const clientContext = await browser.newContext()
const clientPage = await clientContext.newPage()
await clientPage.goto(process.env.NEXTJS_MONITOR_URL)
const clientHeading = await clientPage.getByRole('heading', { level: 1 }).textContent()
expect(clientHeading).toBe(serverHeading)
await serverContext.close()
await clientContext.close()
})Use this as a diagnostic or CI test rather than the primary scheduled monitor. Many pages intentionally update after hydration, so compare an element that should be identical in both renders.
How do you avoid false alerts from console noise?
Do not fail on every console.error. Advertising, analytics, browser extensions, and blocked third-party resources can log errors without breaking the route.
Maintain a narrow reviewed allowlist:
const ignoredErrors = [
/third-party analytics request failed/i,
]
page.on('console', message => {
if (message.type() !== 'error') return
if (ignoredErrors.some(pattern => pattern.test(message.text()))) return
runtimeErrors.push(message.text())
})Every ignored pattern should identify one understood message. Avoid /.*/ patterns or broad host exclusions that hide new failures.
Test the production build in CI
Next.js recommends end-to-end testing for async Server Components because some lower-level test tools do not fully support them. Run Playwright against next build and next start, not only the development server.
The CI run catches hydration regressions before deployment. The scheduled Browser Check catches differences introduced by production data, environment variables, edge middleware, CDN rules, or missing JavaScript chunks.
How to use Hyperping for Next.js hydration monitoring
1. Choose an interactive route
Select a server-rendered route with a stable heading and at least one harmless client interaction. Start with a high-traffic landing, login, or pricing page.
2. Capture hydration errors
Attach console and page-error listeners before navigation. Fail on known React hydration messages and unexpected first-party exceptions.
3. Verify client interaction
Open a menu, follow a client-side link, or trigger validation. Assert the state that proves React event handlers are active.
4. Add the Browser Check
Create a Hyperping Browser Check, paste the script, and set NEXTJS_MONITOR_URL and NEXTJS_APP_ORIGIN as environment variables.
5. Schedule after deployments
Run the check continuously and include it in post-deployment verification. Use the failed trace, video, and console output to separate markup mismatches from missing chunks or APIs.
A rendered page still needs a working interaction
The most useful Next.js check combines three signals: visible server content, no relevant runtime errors, and a successful client interaction. That combination catches the failure mode an HTTP request cannot see.
Read the Playwright assertions guide for retrying UI checks and the network interception guide for diagnosing failed Next.js data requests.
FAQ
Can Playwright detect Next.js hydration errors? ▼
Yes. Playwright can record browser console errors, fail on known hydration messages, and verify that client-side interactions work after the page loads.
Why does a Next.js page return 200 when hydration failed? ▼
The server can return valid HTML before React attaches event handlers in the browser. A hydration mismatch happens during client execution, so an HTTP check can remain green while buttons or navigation fail.
