# navigator.storage.getDirectory() (OPFS)

> How getDirectory() opens the origin private file system, how createSyncAccessHandle() gives a worker synchronous reads and writes, and the lock errors.

`navigator.storage.getDirectory()` resolves to the root `FileSystemDirectoryHandle` of the origin
private file system (OPFS), a quota-managed store of files and directories that no picker or
permission prompt guards and that is not visible to the user in a file manager. Inside a dedicated
worker, `FileSystemFileHandle.createSyncAccessHandle()` adds synchronous, offset-addressed
reads and writes, which is what makes SQLite and other Wasm databases practical in the browser.

## Syntax

```js
navigator.storage.getDirectory()
fileHandle.createSyncAccessHandle()
fileHandle.createSyncAccessHandle({ mode })
```

`getDirectory()` is available in windows and workers (BCD `api.StorageManager.getDirectory`:
Chrome 86, Firefox 111, Safari 15.2). `createSyncAccessHandle()` exists only in dedicated workers
(Chrome 102, Firefox 111, Safari 15.2) and, despite its name, returns a promise; the methods on
the resulting `FileSystemSyncAccessHandle` (`read`, `write`, `getSize`, `truncate`, `flush`,
`close`) are the synchronous part. The asynchronous `createWritable()` on an OPFS file handle
arrived in Safari 26, so on Safari 15.2 to 18 the sync access handle is the only write path.

## Parameters

| Method | Parameter | Type | Meaning |
|---|---|---|---|
| `getDirectory` | none | | Returns the OPFS root for this origin. Each origin, and each storage partition of an embedded origin, has its own root. |
| `createSyncAccessHandle` | `options.mode` | string, optional | Lock mode. `"readwrite"` (default): one handle at a time per file. `"read-only"`: any number of concurrent handles, all limited to `read()`, `getSize()`, and `close()`. `"readwrite-unsafe"`: any number of concurrent writable handles with no coordination; the caller is responsible for consistency. |

Files and directories under the root are reached with `getFileHandle(name, { create })` and
`getDirectoryHandle(name, { create })`, and removed with `removeEntry(name, { recursive })`.
Usage counts against the origin's quota and appears in
[`navigator.storage.estimate()`](/reference/storage/quota-estimate/); clearing site data deletes
the whole tree.

## Exceptions

| Exception | Where | When |
|---|---|---|
| `SecurityError` `DOMException` | `getDirectory()` | The browser cannot map a directory for this origin, for example on an opaque origin or when storage is disabled. |
| `NotFoundError` `DOMException` | `getFileHandle()`, `createSyncAccessHandle()` | The named entry does not exist and `create` was not `true`, or the file was removed after the handle was obtained. |
| `TypeMismatchError` `DOMException` | `getFileHandle()`, `getDirectoryHandle()` | The name exists but is the other kind of entry. |
| `NoModificationAllowedError` `DOMException` | `createSyncAccessHandle()` | The lock is unavailable: another handle is open in `"readwrite"` mode, or a `FileSystemWritableFileStream` is open on the file. The usual cause is a second tab's worker opening the same database file. |
| `InvalidStateError` `DOMException` | `createSyncAccessHandle()`, any method on a closed handle | The handle does not point into the OPFS (a handle from a picker), or `close()` was already called. |
| `NotAllowedError` `DOMException` | `createSyncAccessHandle()` | The permission state for the handle is not `granted` in `"readwrite"` mode. |
| `QuotaExceededError` `DOMException` | `write()`, `truncate()` | The origin's quota is exhausted. The file keeps the bytes written before the failure. |

`write()` returns the number of bytes written and does not guarantee durability until `flush()`
returns; a worker killed between the two can lose the tail of the write.

## Examples

The worker examples assume a dedicated worker created with `new Worker()`; shared workers and
service workers do not get the sync handle.

### Appending to a log file from a dedicated worker

The main thread posts a line; the worker holds one `"readwrite"` handle for the lifetime of the
page and appends at the current size. Opening and closing the handle per message would also work
but pays the lock round-trip each time.

```js
// log-worker.js (dedicated worker)
let handle;

self.onmessage = async ({ data: line }) => {
  if (!handle) {
    const root = await navigator.storage.getDirectory();
    const file = await root.getFileHandle('app.log', { create: true });
    handle = await file.createSyncAccessHandle();
  }
  const bytes = new TextEncoder().encode(`${line}\n`);
  handle.write(bytes, { at: handle.getSize() });
  handle.flush();
};
```

Because the handle is exclusive, a second tab that starts the same worker gets
`NoModificationAllowedError` from `createSyncAccessHandle()`; the page should catch it and either
use `"readwrite-unsafe"` with its own locking (for example the
[Web Locks API](/reference/capabilities/web-locks/)) or route writes through one tab.

### Reading a file back on the main thread

The asynchronous half of the API works in a window. `getFile()` returns a `File`, so the
ordinary `text()`, `arrayBuffer()`, and `stream()` readers apply.

```js
async function readLog() {
  const root = await navigator.storage.getDirectory();
  try {
    const file = await (await root.getFileHandle('app.log')).getFile();
    return await file.text();
  } catch (err) {
    if (err.name === 'NotFoundError') return ''; // nothing logged yet
    throw err;
  }
}
```

A `File` from the OPFS is a snapshot: bytes a worker appends after `getFile()` resolves are not
visible through it, so re-fetch the handle's file for a fresh read.

### Detecting support and choosing a store

`createSyncAccessHandle` is exposed only to dedicated workers, so a check on the main thread is
`false` even in Chrome. Run the probe inside the worker and report the result back; the page
picks the store from the answer.

```js
// probe-worker.js (dedicated worker)
(async () => {
  if (!('storage' in navigator) || typeof navigator.storage.getDirectory !== 'function') {
    postMessage('none'); // no OPFS: the page falls back to IndexedDB
    return;
  }
  const sync = typeof FileSystemFileHandle !== 'undefined'
    && 'createSyncAccessHandle' in FileSystemFileHandle.prototype;
  postMessage(sync ? 'sync' : 'async');
})();
```

`'sync'` means the log-worker path above will work; `'async'` covers an engine that exposes the
root but not the sync handle, where IndexedDB remains the better choice for a database.

:::observed
Chrome DevTools has no OPFS viewer in the Application panel, so the only built-in evidence that a
file exists is the **Usage** figure in Application > Storage changing after a write. Safari's Web
Inspector likewise lists no OPFS entries. The practical check is a one-liner in the Console:
`(await (await (await navigator.storage.getDirectory()).getFileHandle('app.log')).getFile()).size`,
which returns the byte count or throws `NotFoundError: A requested file or directory could not be
found at the time an operation was processed.`, Chromium's default `DOMException` message for
`NotFoundError`.
:::

## See also

- [File System Standard: createSyncAccessHandle()](https://fs.spec.whatwg.org/#api-filesystemfilehandle-createsyncaccesshandle) (whatwg.org)
- [Origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) (developer.mozilla.org)
- [File System Access API](/reference/capabilities/file-system-access/)
- [StorageManager.estimate()](/reference/storage/quota-estimate/)
- [IndexedDB](/reference/storage/indexeddb/)