JavaScript Web Locks Table
| Piece | What it does | Field note |
|---|---|---|
navigator.locks.request(name, cb) | Hold a cross-tab lock | The ONLY cross-tab coordination primitive in the platform |
exclusive (default) | One holder at a time | Migration and single-writer patterns |
{ mode: 'shared' } | Many readers | Shared vs exclusive = the read/write lock you know |
ifAvailable: true | No waiting | Lock OR null - the try-acquire for 'am I the only tab?' |
steal: true | Break a stale holder | For dead-tab recovery - the old holder's promise never resolves |
Lock auto-release | On callback end or tab death | Tab crash releases the lock - no stale locks in storage |
held.getHolderSnapshot | Inspect holders | Debug which tab holds what |
vs localStorage flag | Atomic vs race | A flag can be double-claimed; a lock cannot - that is the point |
Web Locks is the platform's only cross-tab coordination primitive: navigator.locks.request('migration', callback) holds a NAMED lock for the callback's duration - one tab runs the migration, other tabs wait, or check with ifAvailable and proceed differently.
Bottom line: two properties kill the localStorage-flag patterns: locks are ATOMIC (a flag can be double-claimed in a race between two tabs; a lock cannot - the platform serializes the grant), and locks are SELF-RELEASING (tab crash or close releases automatically - no stale flags stuck in storage demanding manual cleanup).
The honest part: locks coordinate TABS on one device on one origin - they are not a distributed lock service, have no network semantics, and vanish with the browser process. Cross-device coordination still needs the server.
How to use
- Single-writer migration: navigator.locks.request('db-migration', async () => { runMigrations(); }) - exactly one tab migrates; the rest simply wait for it to finish.
- Try-acquire without waiting: await navigator.locks.request('leader', { ifAvailable: true }, async lock => { if (!lock) return; beLeader(); }) - 'am I the only tab?' in one call.
- Recover from dead holders: request with { steal: true, ifAvailable: false } - breaks a stale lock whose holder died mid-task, for long-running-job recovery.
Frequently asked questions
What did teams do before Web Locks, and why were the patterns fragile?
localStorage flags and BroadcastChannel elections. The flag pattern: tab A writes 'migration-running = true', other tabs check and skip. It fails on timing (two tabs read false before either writes - both migrate), on crashes (the flag stays true forever after a tab dies mid-migration, blocking all future runs until someone clears it manually), and on clarity (the flag is a hint; nothing enforces it). Web Locks fixes all three structurally: the grant is atomic (the platform serializes requests), release is automatic on tab death, and waiting tabs are genuinely BLOCKED, not politely-informed. The pattern wasn't badly written - it was built on a storage primitive that cannot promise coordination.
How do exclusive and shared modes map to real coordination needs?
Classic read/write locking. Exclusive (the default) means one holder: migrations, single-leader election, 'only this tab writes the cache'. Shared mode means unlimited co-holders but blocks exclusive requests: many tabs reading and validating a shared resource while an updater waits for them all to finish. The semantics compose like database locks - a shared holder arrives while an exclusive waits and the exclusive waits for ALL shared holders to release. The subtlety worth knowing: the mode is per-REQUEST, so the same named lock can be requested shared by five tabs and exclusive by a sixth, and the platform arbitrates the ordering.
When do ifAvailable and steal earn their keep?
ifAvailable is the non-blocking ask: you get the lock OR null immediately - the 'am I the leader?' check, the skip-if-busy background job, the UI that shows 'another tab is editing' instead of queueing. steal: true is the recovery tool: it TAKES the lock even if held, breaking a dead or stuck holder - the long-running job whose tab crashed and whose lock would otherwise block every retry forever (the pre-locks stale-flag problem, solved at the API level). Steal is deliberately violent - the previous holder's callback stays waiting while it believes it holds the lock - so it belongs in recovery paths with explicit human or heartbeat triggers, not in routine acquisition.
What are the boundaries of Web Locks as a coordination tool?
One device, one browser, one origin, one process lifetime. Locks live in the browser's memory: they coordinate tabs of the same origin on the same machine - not across devices, not across browsers, not after the browser restarts. They are also not QUEUES: there is no fairness guarantee about which waiting tab gets the lock next, and no message-passing - the lock says 'your turn', not 'here is the data'. Cross-device coordination, durable job claims and ordered work distribution remain server-side concerns (database transactions, queues). Within its boundary - multi-tab single-device coordination like migrations, leader election and cache rebuild dedupe - it is the only primitive the platform offers, and it does that one job atomically.