The idb Library: Promise-Based IndexedDB
Why a wrapper library?
Section titled “Why a wrapper library?”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.
Opening a database with openDB
Section titled “Opening a database with openDB”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:
| Argument | Type | Purpose |
|---|---|---|
name | string | Database name (scoped to origin) |
version | number | Schema version — increment to trigger upgrades |
{ upgrade(db) } | object | Callback 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.
Writing records with db.put
Section titled “Writing records with db.put”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).
Reading records with db.get and db.getAll
Section titled “Reading records with db.get and db.getAll”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 (orundefinedwhen the key is absent).db.getAll(storeName)returns an array of all records in the store.
Deleting records with db.delete
Section titled “Deleting records with db.delete”await db.delete('tasks', 1);const remaining = await db.getAll('tasks');console.log(remaining.length); // 1db.delete(storeName, key) removes the record for that key. Calling it on a key that does not exist is a no-op.
Runnable: idb in action
Section titled “Runnable: idb in action”Comparing raw IndexedDB vs idb
Section titled “Comparing raw IndexedDB vs idb”The same “open database and write a record” operation looks like this with raw IndexedDB:
// Raw IndexedDB — callback styleconst 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 styleconst 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.