# IDBFactory.open() and the IndexedDB request model

> How indexedDB.open() creates and upgrades a versioned database, why schema changes belong in upgradeneeded, and the errors a PWA must handle.

`indexedDB.open(name, version)` opens a named, versioned, origin-scoped database and returns an
`IDBOpenDBRequest` whose events deliver the connection; every read and write then runs inside an
`IDBTransaction` scoped to named object stores. IndexedDB is the browser's store for structured
records and blobs, reachable from windows, workers, and service workers, and the only one of the
quota-managed stores that supports indexes and range queries.

## Syntax

```js
indexedDB.open(name)
indexedDB.open(name, version)
```

The call returns synchronously with an `IDBOpenDBRequest`. The connection arrives on the
request's `success` event as `request.result`; a version increase fires `upgradeneeded` first,
and `blocked` fires when another connection holds the old version open. Supported in Chrome 23,
Firefox 10, and Safari 8, with `open()` itself stable in Safari since version 15 after the Safari 14
bug in which the first `open()` could hang (BCD `api.IDBFactory.open`, WebKit bug 226547).

## Parameters

| Parameter | Type | Meaning |
|---|---|---|
| `name` | string | The database name, case-sensitive, scoped to the origin. Any string is valid, including the empty string. |
| `version` | unsigned long long, optional | The schema version to open at. Omitted: the existing version is kept, or `1` is used for a new database. Higher than stored: `upgradeneeded` fires with `event.oldVersion` and `event.newVersion`. Lower than stored: the request fails with `VersionError`. |

Schema changes (`createObjectStore()`, `deleteObjectStore()`, `createIndex()`,
`deleteIndex()`) are legal only on the `versionchange` transaction that runs during
`upgradeneeded`. Outside it they throw `InvalidStateError`.

## Exceptions

`open()` itself throws in one case; everything else surfaces on the request or the transaction.

| Error | Surfaces as | When |
|---|---|---|
| `TypeError` | thrown by `open()` | `version` is `0`, negative, `NaN`, or not representable as an unsigned 64-bit integer. |
| `VersionError` | `request.error` | The requested version is lower than the stored one. Pass no version to open at whatever is stored. |
| `AbortError` | `request.error` | The `upgradeneeded` handler threw, or called `transaction.abort()`, so the upgrade rolled back and the connection was not opened. |
| `QuotaExceededError` | `request.error` on the write; the transaction aborts | The origin's quota is exhausted. The whole transaction is rolled back, not just the failing request. |
| `TransactionInactiveError` | thrown by `objectStore.put()` and friends | The transaction has already committed because control returned to the event loop with no pending requests, typically after an `await` on an unrelated promise. Chromium's message is `The transaction has finished.` ([idb_database.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/indexeddb/idb_database.cc), chromium.googlesource.com). |
| `InvalidStateError` | thrown by `createObjectStore()`, `createIndex()` | Called outside a `versionchange` transaction. Chromium's message is `The database is not running a version change transaction.` |
| `ConstraintError` | `request.error` | `add()` with a key that already exists, or an index with `unique: true` whose value is already present. |
| `DataError` | thrown by `put()`, `add()`, `get()` | The key is not a valid key (an object, `undefined`), or a key was passed for a store that has a `keyPath`. |

Transactions auto-commit. A transaction stays alive while it has pending requests; once the last
request's event handler returns without issuing another, it commits. `transaction.commit()`
(Chrome 76, Firefox 74, Safari 15) only makes this explicit and skips the wait for the event loop.

## Examples

Each example wraps the event API in a promise, which is what the `idb` library and similar
wrappers do internally.

### Opening with a migration ladder

Branch on `event.oldVersion` so each schema step runs exactly once, whatever version the user
last had. Creating a store that already exists throws `ConstraintError`, so the ladder, not
`objectStoreNames.contains()`, is the guard.

```js
function openDb() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('notes', 3);
    request.onupgradeneeded = (event) => {
      const db = request.result;
      if (event.oldVersion < 1) {
        db.createObjectStore('drafts', { keyPath: 'id' });
      }
      if (event.oldVersion < 2) {
        request.transaction.objectStore('drafts').createIndex('byUpdated', 'updatedAt');
      }
      if (event.oldVersion < 3) {
        db.createObjectStore('outbox', { autoIncrement: true });
      }
    };
    request.onblocked = () => console.warn('Close other tabs to finish the upgrade');
    request.onsuccess = () => {
      request.result.onversionchange = () => request.result.close();
      resolve(request.result);
    };
    request.onerror = () => reject(request.error);
  });
}
```

The `onversionchange` handler closes this connection when another tab opens a newer version;
without it, the other tab sits in `blocked` until this one is closed.

### Writing without losing the transaction

Issue every request of a transaction synchronously, or chain them from each other's `success`
handler. An `await fetch()` between two `put()` calls lets the transaction commit, and the second
`put()` throws `TransactionInactiveError`.

```js
async function saveDraft(db, draft) {
  const body = await render(draft); // do async work BEFORE opening the transaction
  return new Promise((resolve, reject) => {
    const tx = db.transaction('drafts', 'readwrite');
    tx.objectStore('drafts').put({ ...draft, body, updatedAt: Date.now() });
    tx.oncomplete = () => resolve();
    tx.onabort = () => reject(tx.error); // a quota failure lands here
  });
}
```

Listen on `tx.onabort` rather than only on `request.onerror`: a quota failure aborts the whole
transaction, and `tx.error` carries the `QuotaExceededError` that explains why.

### Detecting support and choosing a fallback store

`indexedDB` is present in every engine in the Syntax section, but it is `undefined` in some
embedded WebViews with storage disabled and throws `SecurityError` on access from an opaque
origin. Decide the store once at startup.

```js
function pickStore() {
  if (!('indexedDB' in self)) {
    return memoryStore(); // no database API: in-memory, lost on reload
  }
  try {
    return idbStore(indexedDB);
  } catch (err) {
    if (err.name === 'SecurityError') return memoryStore(); // opaque origin or storage blocked
    throw err;
  }
}
```

A page that falls back to `localStorage` instead should cap values well under the 5 MiB
Firefox allows per origin; see
[localStorage and sessionStorage](/reference/storage/localstorage/).

:::observed
Chrome DevTools, Application > Storage > IndexedDB, lists each database as `<name> - <origin>`
with its version and object stores; a transaction that was left open by an `await` logs `Uncaught DOMException: Failed to execute 'put' on 'IDBObjectStore': The transaction
has finished.` in the Console, the string defined as `kTransactionFinishedErrorMessage` in
Chromium's [idb_database.cc](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/indexeddb/idb_database.cc)
(chromium.googlesource.com).
:::

## See also

- [Indexed Database API 3.0: open() method](https://www.w3.org/TR/IndexedDB/#dom-idbfactory-open) (w3.org)
- [Working with IndexedDB](https://web.dev/articles/indexeddb) (web.dev)
- [IDBFactory: open() method](https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/open) (developer.mozilla.org)
- [StorageManager.persist() and persisted()](/reference/storage/persistence/)
- [StorageManager.estimate()](/reference/storage/quota-estimate/)
- [Background sync](/reference/service-worker/background-sync/)