microsoft/playwright · error · Error
Recorder is shown, close it before starting a recording.
Error message
Recorder is shown, close it before starting a recording.
What it means
ProgrammaticRecorderApp.start refuses to begin an API-driven recording when the interactive recorder UI is already attached to the context, detected via the recorderAppSymbol on the BrowserContext. The two recorder modes are mutually exclusive per context; close the shown recorder first.
Solutions
- Close the interactive recorder before starting a programmatic recording.
- Start the programmatic recording on a different browser context.
- Reorder your workflow: do all programmatic recordings first, open the interactive recorder afterwards.
Example fix
// before
await context.showRecorder({ mode: 'recording' });
await context.startRecording({ language: 'javascript' }); // throws
// after
await context.close(); // or close the recorder window
const ctx2 = await browser.newContext();
await ctx2.startRecording({ language: 'javascript' }); Defensive patterns
Strategy: try-catch
Try / catch
try {
await context.startRecording({ language: 'python' });
} catch (e) {
if (e.message.includes('Recorder is shown')) {
// close the interactive recorder first, or use a new context
await context.close();
context = await browser.newContext();
await context.startRecording({ language: 'python' });
} else throw e;
} Prevention
- Dedicate separate contexts to interactive codegen and API recording.
- Close the recorder UI as part of teardown before programmatic recording starts.
When it happens
Trigger: Calling context.startRecording (ProgrammaticRecorderApp.start) after showRecorder has opened the interactive RecorderApp on the same context and it has not been closed.
Common situations: Users open codegen manually, then a script tries to start programmatic recording on the same browser context; automation that shows the recorder for inspection and later starts headless recording without closing it.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Recording is already in progress.
- Recording is in progress, stop it before showing the…
- Failed to parse response
- Recording is already in progress.
AI-assisted analysis of microsoft/playwright@500c9c822c (2026-09-15).
Data as JSON: /api/errors/9253429aafaaab21.
Report an issue: GitHub.
Appendix: source
Thrown at packages/playwright-core/src/server/recorder/recorderApp.ts:367
}
export type ProgrammaticRecorderEvent = {
event: 'actionAdded' | 'actionUpdated' | 'signalAdded';
data: any;
page: Page;
code: string;
};
// Each client that records through the api gets its own app, so that clients
// sharing the context only receive events of the recordings they started.
export class ProgrammaticRecorderApp {
private _context: BrowserContext;
private _recorder: Recorder;
private _listeners: RegisteredListener[];
static async start(context: BrowserContext, params: channels.BrowserContextStartRecordingParams, onEvent: (event: ProgrammaticRecorderEvent) => void): Promise<ProgrammaticRecorderApp> {
if ((context as any)[recorderAppSymbol])
throw new Error('Recorder is shown, close it before starting a recording.');
const recorder = await Recorder.forContext(context, { language: params.language, mode: 'recording', recorderMode: 'api', omitCallTracking: true });
const app = new ProgrammaticRecorderApp(context, recorder, params, onEvent);
programmaticRecorderApps(context).add(app);
await recorder.setMode('recording');
return app;
}
private constructor(inspectedContext: BrowserContext, recorder: Recorder, params: channels.BrowserContextStartRecordingParams, onEvent: (event: ProgrammaticRecorderEvent) => void) {
this._context = inspectedContext;
this._recorder = recorder;
let lastAction: actions.ActionInContext | undefined;
let lastActionPage: Page | undefined;
const languages = [...languageSet()];
// Only actions are generated, so the header options do not matter.
const languageGeneratorOptions = {
browserName: inspectedContext._browser.options.name,
launchOptions: { headless: false },View on GitHub (pinned to 500c9c822c)