Skip to content

IndexedDB - Ecmascript Binding

Modernizing IndexedDB with Promises and Async/Await

Section titled “Modernizing IndexedDB with Promises and Async/Await”

The native IndexedDB API is powerful but its event-based, callback-style can be verbose and lead to deeply nested code, often called ‘callback hell’. Modern JavaScript (ECMAScript 2017+) offers features like Promises and async/await that can dramatically simplify asynchronous code, making it more readable and maintainable.

Consider a simple task: opening a database, adding an item, and then reading it back. With the native API, this requires nesting onsuccess handlers, with separate onerror handlers at each level.

const request = indexedDB.open('myDB', 1);
request.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction('store', 'readwrite');
const store = tx.objectStore('store');
const addRequest = store.add({ id: 1, name: 'test' });
addRequest.onsuccess = () => {
console.log('Added!');
// Now let's read it...
const getRequest = store.get(1);
getRequest.onsuccess = (e) => {
console.log('Got item:', e.target.result);
}
getRequest.onerror = (e) => { /* handle get error */ };
}
addRequest.onerror = (e) => { /* handle add error */ };
};

This code is hard to read and even harder to manage for more complex sequences.

The Solution: Wrapping Requests in Promises

Section titled “The Solution: Wrapping Requests in Promises”

A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation. We can create a helper function that takes an IDBRequest and returns a Promise.

function promisifyRequest(request) {
return new Promise((resolve, reject) => {
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}

This simple wrapper allows us to use .then() for success and .catch() for errors, which is a major improvement.

The async/await syntax, built on top of Promises, lets us write asynchronous code that looks and behaves like synchronous code. This is the modern standard for handling async operations in JavaScript.

Let’s refactor our ‘add and get’ task into a clean async function. Notice how try...catch provides unified error handling.

// Helper function from above
function promisifyRequest(request) {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// We also need to promisify transactions
function promisifyTransaction(tx) {
return new Promise((resolve, reject) => {
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
}
async function addAndGetItem(db, item) {
try {
const tx = db.transaction('store', 'readwrite');
const store = tx.objectStore('store');
await promisifyRequest(store.add(item));
console.log('Item added!');
const retrievedItem = await promisifyRequest(store.get(item.id));
console.log('Retrieved item:', retrievedItem);
await promisifyTransaction(tx);
console.log('Transaction complete.');
return retrievedItem;
} catch (error) {
console.error('An error occurred:', error);
throw error; // Re-throw error for the caller to handle
}
}
// Usage:
// const db = await promisifyRequest(indexedDB.open('myDB', 1));
// await addAndGetItem(db, { id: 1, name: 'test' });

While writing your own promise wrappers is a great learning exercise, for real-world projects, it’s highly recommended to use a well-tested wrapper library. The most popular one is idb by Jake Archibald.

It provides a tiny, promise-based layer over IndexedDB that makes all operations incredibly simple, while still giving you full access to the underlying API when needed. You can add it to your project via npm: npm install idb.

Example: The Same Task with the idb Library

Section titled “Example: The Same Task with the idb Library”

Compare the verbosity of the previous examples with the elegance and clarity of this version using idb.

import { openDB } from 'idb';
async function addAndGetItemWithIdb(item) {
try {
const db = await openDB('myDB', 1, {
upgrade(db) {
db.createObjectStore('store', { keyPath: 'id' });
},
});
// The library handles transactions for you!
await db.add('store', item);
console.log('Item added!');
const retrievedItem = await db.get('store', item.id);
console.log('Retrieved item:', retrievedItem);
return retrievedItem;
} catch (error) {
console.error('An error occurred:', error);
}
}
// Usage:
// await addAndGetItemWithIdb({ id: 1, name: 'test' });

For any serious IndexedDB development, adopting modern practices like async/await and using a helper library like idb is not just a recommendation—it’s essential for building robust and maintainable applications.