Skip to content

The idb Library: Promise-Based IndexedDB

The raw IndexedDB API is entirely callback-driven. Every operation — opening a database, writing a record, reading it back — requires you to listen for onsuccess and onerror events and nest the next operation inside the previous callback. Real-world code becomes a pyramid of nested handlers.

The idb library by Jake Archibald is a tiny (~1 KB gzipped) wrapper that mirrors the full IndexedDB API but returns Promises for every operation. That means you can use async/await throughout, the same way you would with fetch or any modern API.

import { openDB } from 'https://esm.sh/idb@8';
const db = await openDB('my-database', 1, {
upgrade(db) {
db.createObjectStore('tasks', { keyPath: 'id' });
},
});

openDB takes three arguments:

ArgumentTypePurpose
namestringDatabase name (scoped to origin)
versionnumberSchema version — increment to trigger upgrades
{ upgrade(db) }objectCallback that runs when the DB is created or the version increases

The upgrade callback is the only place where you can create or delete object stores. It is skipped entirely on subsequent opens when the version has not changed.

await db.put('tasks', { id: 1, title: 'Buy groceries', done: false });
await db.put('tasks', { id: 2, title: 'Write tests', done: true });

db.put(storeName, value) inserts the value if the key does not exist, or replaces it if it does. The key is inferred from the keyPath you defined when creating the store ('id' in the example above).

const task = await db.get('tasks', 1);
// { id: 1, title: 'Buy groceries', done: false }
const allTasks = await db.getAll('tasks');
// [ { id: 1, ... }, { id: 2, ... } ]
  • db.get(storeName, key) returns a single record (or undefined when the key is absent).
  • db.getAll(storeName) returns an array of all records in the store.
await db.delete('tasks', 1);
const remaining = await db.getAll('tasks');
console.log(remaining.length); // 1

db.delete(storeName, key) removes the record for that key. Calling it on a key that does not exist is a no-op.

Browser Storage

The same “open database and write a record” operation looks like this with raw IndexedDB:

// Raw IndexedDB — callback style
const request = indexedDB.open('my-db', 1);
request.onupgradeneeded = (e) => {
e.target.result.createObjectStore('tasks', { keyPath: 'id' });
};
request.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction('tasks', 'readwrite');
const store = tx.objectStore('tasks');
const putReq = store.put({ id: 1, title: 'Buy groceries', done: false });
putReq.onsuccess = () => console.log('Saved');
};
request.onerror = (e) => console.error(e.target.error);

And with idb:

// idb — async/await style
const db = await openDB('my-db', 1, {
upgrade(db) {
db.createObjectStore('tasks', { keyPath: 'id' });
},
});
await db.put('tasks', { id: 1, title: 'Buy groceries', done: false });
console.log('Saved');

Both do exactly the same thing. The idb version is shorter, easier to read, and errors surface as rejected Promises that you can catch with a standard try/catch.

What is the third argument to openDB() used for?
What does db.put(storeName, value) do when the key already exists in the store?
Which idb method returns ALL records in an object store as an array?
What is the primary advantage of the idb library over the raw IndexedDB API?