How to fix net::ERR_ABORTED during page.goto in Playwright?
Playwrightpage.goto: net::ERR_ABORTED means the browser started a navigation but the request was cancelled before a response was received. A frequent variant of the full error message is page.goto: net::ERR_ABORTED; maybe frame was detached?, which Playwright appends as a hint when it suspects the abort was caused by the frame being removed during navigation — for example, when an SPA replaces the root frame or a redirect chain removes the page before the new document loads. Other causes include a page.route() handler calling route.abort(), a page.close() called during navigation, or a redirect that sends the browser to a URL your test didn't expect.
Common mistake
test('loads dashboard after login', async ({ page }) => {
// Triggers a redirect chain: /login -> /oauth -> /dashboard
await page.goto('https://app.example.com/login');
// Second goto fires while first redirect is still in flight
await page.goto('https://app.example.com/dashboard');
});
Firing two navigations in quick succession will abort the first one, which can cascade into the error depending on timing.
The fix
For overlapping navigations — wait for the URL you want:
import { test, expect } from '@playwright/test';
test('loads dashboard via redirect', async ({ page }) => {
await page.goto('https://app.example.com/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait for the final destination — handles any redirect chain
await page.waitForURL('**/dashboard', { timeout: 15000 });
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
For the "maybe frame was detached" variant — avoid acting on frames that may be replaced:
// Instead of going directly to a URL inside an SPA that rebuilds its frame
await page.goto('https://app.example.com');
await page.getByRole('link', { name: 'Settings' }).click();
await page.waitForURL('**/settings');
For route handlers that accidentally abort requests:
await page.route('**/api/**', async (route) => {
// Always either fulfill, continue, or abort — never leave hanging
if (route.request().url().includes('/telemetry')) {
await route.abort();
} else {
await route.continue();
}
});
Why it works
ERR_ABORTED is a Chromium-level signal that the network request was cancelled at the browser's request — not a server-side failure. Chaining page.waitForURL after a click that triggers navigation ensures you only proceed after the browser has settled on the target URL, eliminating races where a second navigation fires into an in-flight first one. For the frame-detached variant, navigating through the UI rather than jumping directly to deep URLs avoids the SPA race where the router removes and reconstructs the root frame.
Tips
- Inspect the page.on('requestfailed') event during debugging to identify which exact request was aborted and what reason code Chromium reported.
- If you have a page.route() handler, verify every code path calls route.fulfill(), route.continue(), or route.abort() — unresolved route handlers can cause aborted navigations when the test runner tears down.
- The "maybe frame was detached" hint points to frame was detached — if your test uses iframes, check that page.
- For offline-mode or network-failure scenarios that produce ERR_ABORTED as a secondary error, see ERR_INTERNET_DISCONNECTED.