# Web Locks API

> navigator.locks.request() runs a callback while holding a named exclusive or shared lock across tabs and workers. LockOptions, every rejection, examples.

`navigator.locks.request()` asks the browser's lock manager for a named lock, runs a callback while the lock is held, and releases it when the callback's promise settles. Tabs, iframes, and workers of one origin that share a storage bucket queue on the same names, so a PWA can serialise writes to IndexedDB, elect one tab to hold a WebSocket, or stop two service-worker clients from running the same migration.

Support is wide: Chrome 69, Edge 79, Firefox 96, Safari 15.4 on macOS and iOS, Samsung Internet 10.0, and Android WebView 69 all implement `LockManager` with `request()` and `query()` (BCD `api.LockManager`). The interface is `[SecureContext, Exposed=(Window,Worker)]`, so it is reachable from dedicated, shared, and service workers but absent on plain `http://` origins other than `localhost`.

## Syntax

```js
navigator.locks.request(name, callback)
navigator.locks.request(name, options, callback)

navigator.locks.query()
```

`request()` returns a `Promise<any>` that settles with whatever `callback` returned or threw, after the lock has been released. A synchronous callback is wrapped in an immediately resolved promise, so the lock is held only for the synchronous run. `query()` returns a `Promise<LockManagerSnapshot>` describing every held and pending lock for the origin's lock manager at the moment of the snapshot.

## Parameters

`request()` takes a resource name, an optional `LockOptions` dictionary, and a callback; in both forms the callback is the last argument.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | `DOMString` | Yes | Resource name chosen by the application. Names starting with `-` are reserved for the browser and rejected. |
| `options` | `LockOptions` | No | Mode and waiting behaviour; see the members below. |
| `callback` | `LockGrantedCallback` | Yes | `(lock) => Promise<any> \| any`, called with a `Lock` (`name`, `mode`) once granted, or with `null` when `ifAvailable` is `true` and the lock could not be granted at once. |

`LockOptions` has four members, all optional.

| Member | Type | Default | Description |
|---|---|---|---|
| `mode` | `LockMode` (`"exclusive"` or `"shared"`) | `"exclusive"` | Several contexts may hold `"shared"` locks on one name at the same time; an `"exclusive"` holder blocks every other request for that name until it releases. |
| `ifAvailable` | `boolean` | `false` | Grant only if no waiting is needed; otherwise invoke `callback` with `null` instead of queueing. Still asynchronous, since the check usually crosses a process boundary. |
| `steal` | `boolean` | `false` | Release any current holders of the name (their `request()` promises reject with `AbortError`), skip the queue, and grant this request. Only valid with `"exclusive"` mode. |
| `signal` | `AbortSignal` | none | Abort the request while it is still queued; the signal is ignored once the lock is granted. |

`query()` takes no parameters. Its snapshot has `held` and `pending` arrays of `LockInfo` objects, each with `name`, `mode`, and the `clientId` of the frame or worker (the same value as a service-worker `Client.id`).

## Exceptions

`request()` rejects before any lock is queued when one of the following holds. The order matches the specification's method steps.

| Exception | Condition |
|---|---|
| `InvalidStateError` | The calling document is not fully active (for example a detached iframe). |
| `SecurityError` | No lock manager can be obtained for the environment: an opaque origin such as a sandboxed iframe without `allow-same-origin`, or a context the browser excludes from the Locks API. |
| `NotSupportedError` | `name` starts with U+002D HYPHEN-MINUS (`-`); `steal` and `ifAvailable` are both `true`; `steal` is `true` with a `mode` other than `"exclusive"`; or `signal` is given together with `steal` or `ifAvailable`. |
| `AbortError` (or the signal's abort reason) | `signal` was already aborted when `request()` was called, or is aborted while the request is still pending. |
| `AbortError` | Another request with `steal: true` took the lock away from this holder. |

`query()` rejects with the same `InvalidStateError` and `SecurityError` conditions and nothing else. Neither method throws synchronously; whatever the callback throws becomes the rejection reason of the `request()` promise after the lock is released.

:::observed
Chrome rejects `navigator.locks.request('-x', () => {})` with `NotSupportedError: Names cannot start with '-'.`, and `request('x', { steal: true, ifAvailable: true }, cb)` with `NotSupportedError: The 'steal' and 'ifAvailable' options cannot be used together.`. In a sandboxed `<iframe sandbox>` without `allow-same-origin` the call rejects with `SecurityError: Access to the Locks API is denied in this context.`, and a holder displaced by `steal: true` sees its `request()` promise reject with `AbortError: Lock broken by another request with the 'steal' option.`. The strings come from Chromium's [`lock_manager.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/locks/lock_manager.cc) and [`lock.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/locks/lock.cc) (chromium.googlesource.com).
:::

## Examples

Each example feature-detects `navigator.locks` and shows the branch taken when it is missing; on the browsers listed above the fallback runs only on insecure origins.

### Serialising an IndexedDB migration across tabs

Two tabs that open the same database can both decide the schema needs upgrading. Wrapping the upgrade in an exclusive lock makes the second tab wait and then find the work already done. Without lock support the function runs the upgrade directly, which is what the code did before the lock existed.

```js
async function withLock(name, work) {
  if (!('locks' in navigator)) return work();
  return navigator.locks.request(name, work);
}

await withLock('db-migration', async () => {
  const version = await readSchemaVersion();
  if (version < 3) await migrateTo3();
});
```

The callback's return value becomes the value of the outer promise, so `withLock()` can return data read under the lock without a second round trip.

### Giving up after a timeout with an AbortSignal

A request that queues behind a tab stuck in a long callback would otherwise wait forever. Pass an `AbortSignal` and abort it from a timer; the rejection carries the reason given to `abort()`, or an `AbortError` when none is given.

```js
async function requestWithTimeout(name, ms, work) {
  if (!('locks' in navigator)) return work();
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(new DOMException('Lock wait exceeded', 'TimeoutError')), ms);
  try {
    return await navigator.locks.request(name, { signal: controller.signal }, work);
  } catch (err) {
    if (err.name === 'TimeoutError') return null;
    throw err;
  } finally {
    clearTimeout(timer);
  }
}
```

Once the lock has been granted the signal is ignored, so aborting after that point does not interrupt `work()`.

### Letting readers share a cache while one writer refreshes it

Shared locks let any number of readers proceed together, while a writer waits for them to finish and then blocks new readers. `ifAvailable: true` lets the refresh step skip its turn when a writer already holds the lock, rather than queueing a duplicate refresh.

```js
async function readCatalog() {
  if (!('locks' in navigator)) return loadCatalogFromCache();
  return navigator.locks.request('catalog', { mode: 'shared' }, () => loadCatalogFromCache());
}

async function refreshCatalog() {
  if (!('locks' in navigator)) return fetchAndStoreCatalog();
  return navigator.locks.request('catalog', { ifAvailable: true }, async (lock) => {
    if (!lock) return 'skipped';
    await fetchAndStoreCatalog();
    return 'refreshed';
  });
}
```

`navigator.locks.query()` shows which `clientId` holds `catalog` and in which mode when a refresh keeps returning `'skipped'`.

## See also

- [Storage Buckets API](/reference/capabilities/storage-buckets/), the boundary that decides which contexts share a lock manager
- [IndexedDB](/reference/storage/indexeddb/), the store most lock names end up protecting
- [The Clients API](/reference/service-worker/clients-api/), where the `clientId` values in a `query()` snapshot come from
- [Web Locks API: request() method](https://w3c.github.io/web-locks/#api-lock-manager-request) (w3.org)
- [Web Locks API: LockOptions](https://w3c.github.io/web-locks/#dictdef-lockoptions) (w3.org)
- [Web Locks API (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API) (developer.mozilla.org)