DEV Community

Mohammed Taukir Sheikh
Mohammed Taukir Sheikh

Posted on

Singleton Pattern issue in playwright browser

Explaining why the singleton pattern retains the previous value:

Why singleton pattern using previous value

The singleton pattern structure:

// ONE instance created when module loads
const playwrightManager = new PlaywrightBrowserManager();

// This instance has properties:
// - this.browser = null (initially)
// - this.context = null (initially)
Enter fullscreen mode Exit fullscreen mode

The problem flow:

Scenario 1: First request

// Request A comes in
playwrightManager.launchBrowser({ headless: false })
   this.browser = BrowserInstance1 (headless: false) 
   Browser window opens
Enter fullscreen mode Exit fullscreen mode

Scenario 2: Second request (the problem)

// Request B comes in 2 seconds later
playwrightManager.launchBrowser({ headless: true })
   Checks: Does this.browser exist? YES (BrowserInstance1)
   Checks: Is it connected? YES (still running)
   Checks: Does headless match? NO (false vs true)
   BUT: BrowserInstance1 was ALREADY launched with headless=false
   BrowserInstance1 CANNOT change its headless setting after launch!
   Returns the existing BrowserInstance1 (still headless: false) 
Enter fullscreen mode Exit fullscreen mode

Why you can't change headless after launch:

// When you launch a browser:
const browser = await chromium.launch({ headless: false });
// The browser is NOW running with headless=false
// You CANNOT do: browser.setHeadless(true) ❌ (doesn't exist)

// The headless setting is baked into the browser process
// Once launched, it's fixed until you close and relaunch
Enter fullscreen mode Exit fullscreen mode

Visual representation:

┌─────────────────────────────────────────────┐
│  Singleton Instance (ONE for entire app)   │
│  ┌───────────────────────────────────────┐ │
│  │ this.browser = BrowserInstance1      │ │ ← Shared by ALL requests
│  │   - headless: false (fixed forever)  │ │
│  │   - launched at: 10:00:00            │ │
│  │   - still connected: YES             │ │
│  └───────────────────────────────────────┘ │
│                                            │
│  Request A (10:00:00):                    │
│    launchBrowser({headless: false})        │
│    → Creates BrowserInstance1 ✅          │
│                                            │
│  Request B (10:00:02):                    │
│    launchBrowser({headless: true})        │
│    → Sees BrowserInstance1 exists         │
│    → Tries to reuse it                    │
│    → But it's still headless: false ❌    │
└─────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The code that caused the issue:

// In the old singleton code:
if (this.browser && this.browser.isConnected()) {
  // Browser exists and is running
  if (currentHeadless === headless) {
    // Settings match - reuse browser ✅
    return { browser: this.browser, context: this.context };
  } else {
    // Settings DON'T match, but browser is still connected
    // What happens? It might still return the old browser!
    // OR it creates a new one but doesn't close the old one properly
  }
}
Enter fullscreen mode Exit fullscreen mode

Why separate instances fix it:

// Request A
const managerA = playwrightManager();  // NEW instance
managerA.launchBrowser({ headless: false })
   managerA.browser = BrowserInstance1 (headless: false) 

// Request B (simultaneously)
const managerB = playwrightManager();  // DIFFERENT instance
managerB.launchBrowser({ headless: true })
   managerB.browser = BrowserInstance2 (headless: true) 
   No conflict! Each has its own browser
Enter fullscreen mode Exit fullscreen mode

Key points:

  1. Browser headless setting is immutable after launch — you can't change it.
  2. Singleton shares one browser instance — all requests use the same this.browser.
  3. Reuse logic — if a browser exists and is connected, it may be reused even with different settings.
  4. Timing — if Request A's browser is still running when Request B starts, Request B might get the old browser.

The fix:

With separate instances:

  • Each request gets its own PlaywrightBrowserManager instance
  • Each instance has its own this.browser property
  • No sharing = no conflicts
  • Each request controls its own browser lifecycle

That's why separate instances solve the problem — each request gets a fresh browser manager with its own browser instance, so settings aren't shared or reused incorrectly.

Top comments (0)