The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Request an exclusive lock | await navigator.locks.request('sync', async lock => runSync()) | View examples |
| Detect Web Locks | if (!navigator.locks) useFallback() | View examples |
| Return a callback value | const result = await navigator.locks.request('cache', () => rebuildCache()) | View examples |
| Request a shared lock | await navigator.locks.request('catalog', { mode: 'shared' }, readCatalog) | View examples |
| Try without waiting | await navigator.locks.request('leader', { ifAvailable: true }, lock => lock && lead()) | View examples |
| Bound lock waiting | await navigator.locks.request('sync', { signal: AbortSignal.timeout(5000) }, synchronize) | View examples |
| Cancel a pending request | controller.abort(new DOMException('Navigation', 'AbortError')) | View examples |
| Hold leadership | await navigator.locks.request('primary', () => leadershipLifetime) | View examples |
| Broadcast state | const channel = new BroadcastChannel('app-coordination') | View examples |
| Close messaging | channel.close() | View examples |
| Order nested locks | for (const name of [...names].sort()) await acquire(name) | View examples |
| Snapshot lock state | const { held, pending } = await navigator.locks.query() | View examples |
| Use a storage transaction | const transaction = database.transaction(['queue'], 'readwrite') | View examples |
| Avoid reserved names | const lockName = `document:${documentId}` | View examples |
The Web Locks API serializes cooperating same-site contexts that share a storage bucket. A named lock is an application-defined scheduling primitive, not an operating-system lock, server mutex, security boundary, or durable transaction. Keep callbacks short, abort pending requests, use storage-native transactions for data integrity, and design recovery because a tab can disappear at any point.
Step by step
Detailed examples
Understand storage-bucket scope and callback lifetime
navigator.locks is exposed to windows and workers in secure contexts. Lock names coordinate agents sharing the same storage bucket in one user agent; they do not cross browsers, profiles, devices, origins, or servers. A lock is held until the callback's returned promise settles, including rejection, then releases automatically. The request promise adopts the callback result. Name resources with stable application identifiers but never place secrets in diagnostic names.
async function rebuildOnce() {
if (!navigator.locks) return rebuildCache();
return navigator.locks.request('cache:v3:rebuild', async () => {
if (await cacheIsCurrent()) return 'already-current';
await rebuildCache();
return 'rebuilt';
});
} Choose exclusive, shared, or conditional acquisition
Exclusive is the default and conflicts with every lock of the same name. Shared mode permits concurrent shared holders but waits behind exclusive work, modeling readers and writers. ifAvailable never waits for conflicting holders and invokes the callback with null; it is still asynchronous. Do not combine ifAvailable with signal or steal. Scheduling is queue-oriented but applications should not turn unspecified timing into correctness assumptions.
await navigator.locks.request(
'maintenance',
{ ifAvailable: true },
async lock => {
if (!lock) return false;
await compactLocalData();
return true;
}
); Bound waiting and cancel work separately after acquisition
A signal aborts only while a request is pending; once granted it is ignored. If work itself must stop, use a separate application signal inside the callback. AbortSignal.timeout() is convenient but feature-test it or use AbortController with a cleared timer. The steal option preempts holders and can leave their code executing without exclusivity, so reserve it for carefully designed recovery protocols, not ordinary timeouts.
const queueController = new AbortController();
const timer = setTimeout(() => queueController.abort(), 5000);
try {
await navigator.locks.request('sync', { signal: queueController.signal }, async () => {
clearTimeout(timer);
await synchronize({ signal: workController.signal });
});
} finally { clearTimeout(timer); } Combine ephemeral leadership with explicit messaging
A long-lived unresolved callback can designate one tab as leader, and browser termination releases its lock so a queued contender may take over. Leadership is only local and cooperative; the server must still deduplicate jobs and enforce leases for cross-device work. BroadcastChannel distributes state but messages are neither authenticated across same-origin scripts nor durable, and storage partitioning can limit communication. Close the channel and resolve the leadership promise during orderly shutdown.
const channel = new BroadcastChannel('sync:v1');
let relinquish;
const lifetime = new Promise(resolve => { relinquish = resolve; });
navigator.locks.request('sync:leader', async () => {
channel.postMessage({ type: 'leader-ready' });
startLeaderWork();
await lifetime;
stopLeaderWork();
});
addEventListener('pagehide', () => { relinquish(); channel.close(); }, { once: true }); Avoid nested cycles and keep critical sections bounded
Web Locks does not detect deadlock. If tasks need multiple names, acquire them in one documented global order and avoid waiting for code that may need a held lock. Never hold a lock across user prompts, indefinite network operations, or a message round trip without timeout and recovery. Resource names beginning with a hyphen are reserved. Release occurs automatically on callback settlement or agent termination, but abrupt termination can leave external side effects incomplete.
async function withDocumentAndIndex(documentId, task) {
const names = [`document:${documentId}`, 'search-index'].sort();
return navigator.locks.request(names[0], () =>
navigator.locks.request(names[1], task)
);
} Use query for diagnostics and storage transactions for correctness
query() returns held and pending arrays with name, mode, and opaque clientId, but the snapshot can be stale immediately. Never check query() and then act as if a lock was acquired; request the lock. A Web Lock coordinates compliant code only and offers no rollback or crash durability. Put IndexedDB reads and writes in one transaction and make external operations idempotent. Treat lock state as potentially sensitive operational telemetry.
async function lockDiagnostics() {
if (!navigator.locks?.query) return null;
const snapshot = await navigator.locks.query();
return {
held: snapshot.held.map(({ name, mode }) => ({ name, mode })),
pending: snapshot.pending.map(({ name, mode }) => ({ name, mode })),
};
} Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



