{"record":{"id":"b2ac0f257ee4df5c","repo":"vercel/next.js","slug":"an-instant-scope-is-already-active-nesting-inst","errorCode":null,"errorMessage":"An instant() scope is already active. Nesting instant() calls is not supported. Did you forget to await the previous instant() call?","messagePattern":"An instant\\(\\) scope is already active\\. Nesting instant\\(\\) calls is not supported\\. Did you forget to await the previous instant\\(\\) call\\?","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/next-playwright/src/index.ts","lineNumber":77,"sourceCode":" * automatically. For a fresh page (before any navigation), pass\n * `baseURL` so the cookie can be scoped to the correct domain:\n *\n *   await instant(page, async () => {\n *     await page.goto(url)\n *     // ...\n *   }, { baseURL: 'http://localhost:3000' })\n *\n * When `@playwright/test` is installed, acquire/release actions appear\n * as labeled steps in the Playwright UI.\n */\nexport async function instant<T>(\n  page: PlaywrightPage,\n  fn: () => Promise<T>,\n  options?: { baseURL?: string }\n): Promise<T> {\n  const context = page.context()\n  if (contextsWithActiveScope.has(context)) {\n    throw new Error(\n      'An instant() scope is already active. Nesting instant() ' +\n        'calls is not supported. Did you forget to await the ' +\n        'previous instant() call?'\n    )\n  }\n\n  // Resolve the cookie's scope before touching any browser state, so misuse on\n  // a fresh page (no baseURL and no prior navigation) fails with the\n  // descriptive error from resolveURL rather than half-entering a scope.\n  const { hostname } = new URL(resolveURL(page, options))\n\n  contextsWithActiveScope.add(context)\n  try {\n    // A completed prior scope on this context can leave the cookie behind (its\n    // client-side release races an in-flight captured-cookie write from a\n    // locked MPA page load; see the note above). No scope is active for this\n    // context, so a present cookie is always stale here — clear it before\n    // acquiring so a completed prior scope never blocks this one.","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/vercel/next.js/blob/0ae8c72462952df163f1b1e0726641bc5b40dc93/packages/next-playwright/src/index.ts#L59-L95","documentation":"Thrown by the `instant()` test helper in next-playwright when an instant() scope is already active on the same browser context. The lock is per-context (tracked via a WeakSet) because the instant-navigation cookie is shared per context. Nesting is unsupported because two scopes would compete for the same cookie/lock. The message hints the most common cause: forgetting to await the previous call.","triggerScenarios":"Calling `instant(page, fn1)` and then calling `instant(page, fn2)` (or the same page's context) before fn1 has resolved — e.g., missing `await`, or calling instant() inside another instant() callback.","commonSituations":"Forgot to `await` the first `instant()` call; sequential instant() calls in a loop where one rejects silently; calling instant() inside another instant() callback body; using the same browser context for parallel instant() calls.","solutions":["Ensure every `instant()` call is fully awaited before starting the next one on the same context.","Do not nest instant() calls — move the inner logic outside the scope or run it after the outer scope completes.","If you genuinely need concurrent instant scopes, use separate browser contexts (separate pages from different contexts).","Wrap instant() calls in try/finally so an early throw doesn't leave the scope active."],"exampleFix":"// before — second call overlaps the first\nawait instant(page, async () => { /* ... */ }) // missing await was here\nawait instant(page, async () => { /* ... */ })\n\n// after — every call awaited\nawait instant(page, async () => { /* ... */ })\nawait instant(page, async () => { /* ... */ })","handlingStrategy":"validation","validationCode":"// The library tracks active scopes internally via WeakSet.\n// Caller-side: ensure no overlapping instant() calls by serializing them.\nconst instantQueue: Array<() => Promise<void>> = []\nlet instantRunning = false\nasync function safeInstant(page, fn) {\n  // Serialize instant() calls on the same context\n  await new Promise<void>(resolve => {\n    instantQueue.push(async () => { resolve(); await fn() })\n    if (!instantRunning) drain()\n  })\n}\nasync function drain() {\n  instantRunning = true\n  while (instantQueue.length) { await instantQueue.shift()!() }\n  instantRunning = false\n}","typeGuard":null,"tryCatchPattern":"try {\n  await instant(page, async () => { /* test code */ })\n} catch (e) {\n  if (e.message.includes('already active')) {\n    // A previous instant() didn't finish — await it first\n    await new Promise(r => setTimeout(r, 100))\n  }\n  throw e\n}","preventionTips":["Always `await` every instant() call before starting the next one on the same page/context.","Never call instant() inside another instant() callback.","Use separate browser contexts if you need concurrent instant scopes.","Wrap instant() in try/finally so exceptions don't leave a scope in a bad state."],"tags":["testing","playwright","instant-navigation","async","concurrency"],"analyzedSha":"0ae8c72462952df163f1b1e0726641bc5b40dc93","analyzedAt":"2026-08-06T19:44:29.143Z","schemaVersion":2},"datasetVersion":"2026-08-07T02:17:10.218Z"}