Skip to content

[Bug]: Non-deterministic crashes when loading config concurrently in ESM - Cannot add property metadata, object is not extensible #41726

Closed

Description

Version

latest

Steps to reproduce

Here's a minimal bash repro Claude helped me set up

mkdir pw-repro && cd pw-repro
npm init -y
npm pkg set type=module
npm i -D @playwright/test
mkdir e2e && printf "import { test } from '@playwright/test'\ntest('noop', () => {})\n" > e2e/noop.spec.ts
printf "import { defineConfig } from '@playwright/test'\nexport default defineConfig({ testDir: './e2e' })\n" > playwright.config.ts

cat > race.sh <<'SH'
#!/usr/bin/env bash
export TMPDIR="$PWD/.tmp"; mkdir -p "$TMPDIR"
for round in $(seq 1 200); do
  rm -rf "$TMPDIR"/playwright-transform-cache-*   # force a cold, concurrent write each round
  for i in $(seq 1 8); do npx playwright test --list >>out.log 2>&1 & done
  wait
  if grep -q "not extensible" out.log; then echo "REPRODUCED at round $round"; break; fi
done
SH
chmod +x race.sh && : > out.log && ./race.sh
grep "not extensible" out.log | head -1

cd .. && rm -rf pw-repro

It fails as expected (although it's not deterministic by nature):

output
frodi@Frodi-Karlsson ~/Development> sh repro.sh
Wrote to /Users/frodi/Development/pw-repro/package.json:

{
  "name": "pw-repro",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs"
}



added 3 packages, and audited 4 packages in 5s

found 0 vulnerabilities
REPRODUCED at round 8
TypeError: Cannot add property metadata, object is not extensible

Expected behavior

No crash

Actual behavior

TypeError: Cannot add property metadata, object is not extensible

Additional context

This occurred in our CI runners, so it's a real-life failure and not just hypothetical.

I think this is what's happening:

Playwright caches transformed TS at $TMPDIR/playwright-transform-cache-<uid>/…js, keyed by hash(filename+content)

packages/playwright/src/transform/compilationCache.ts has a fs.writeFileSync(codePath, code, 'utf8') that will truncate the file to zero length then fill it

packages/playwright/src/transform/transform.ts has if (cachedCode !== undefined) return { code: cachedCode, … } ('' is !== undefined)

So when two processes load the same config concurrently, one can read the cache file mid-write and get an empty string, meaning an empty module. With an ESM config, that empty module is an ES module namespace, and since it has no default export, loadUserConfig's object['default'] unwrap is skipped, so userConfig is the namespace itself.

config.ts then runs userConfig.metadata = userConfig.metadata || {} on that namespace, we get TypeError: Cannot add property metadata, object is not extensible

Suggested fix:

Make the cache write atomic, perhaps via writing to a tmp file and renaming. Happy to contribute with a PR.

Environment

frodi@Frodi-Karlsson ~/Development> npx envinfo --preset playwright


  System:
    OS: macOS 15.5
    CPU: (11) arm64 Apple M3 Pro
    Memory: 127.13 MB / 18.00 GB
  Binaries:
    Node: 24.18.0 - /Users/frodi/.local/state/fnm_multishells/41576_1783683870601/bin/node
    Yarn: 1.22.22 - /Users/frodi/Library/pnpm/yarn
    npm: 11.16.0 - /Users/frodi/.local/state/fnm_multishells/41576_1783683870601/bin/npm
    bun: 1.3.3 - /Users/frodi/.bun/bin/bun
    Deno: 2.7.7 - /opt/homebrew/bin/deno
  IDEs:
    VSCode: 1.127.0 - /usr/local/bin/code
    Claude Code: 2.1.185 - /opt/homebrew/bin/claude
  Languages:
    Bash: 3.2.57 - /bin/bash

Activity

  1. yury-s commented on Jul 10, 2026

    @yury-s
    Member

    Running several playwright test processes concurrently against the same config isn't a supported setup. The transform cache lives under $TMPDIR per user and is keyed by the config's absolute path, so two cold loads from the same checkout race on the same cache file — that's the crash. If you need to run them in parallel, use separate checkouts: different paths mean separate caches and no collision. For an immediate unblock without that, point each invocation at its own temp dir (TMPDIR=/tmp/pw-<unique>, unique per process), which avoids the cache race — but that only fixes this specific error; concurrent full runs in one directory will still clobber each other's test-results/ and report output, so separate checkouts are the real answer. Closing as won't-fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions