Introduction
IndexedDB is asynchronous by design — there is no way to make a truly synchronous call to it from the main thread. The browser intentionally prevents synchronous database access to avoid blocking the UI. The solution is to use Promises and async/await to write code that reads like synchronous code but executes asynchronously. If you genuinely need synchronous IndexedDB access, it was available in Web Workers via indexedDB.openSync() in an early spec, but no browser ever implemented it.
Why IndexedDB Is Async-Only
IndexedDB operations involve disk I/O. A synchronous disk read on the main thread would freeze the page — no scrolling, no animations, no click handlers — until the read completes. This can take 1-100ms or more. The browser API forces asynchronous access to guarantee a responsive UI.
1// IndexedDB uses request objects with onsuccess/onerror callbacks
2const request = db.transaction('store').objectStore('store').get('key');
3request.onsuccess = () => console.log(request.result);
4request.onerror = () => console.error(request.error);
Wrapping IndexedDB in Promises
Convert the callback-based API into Promises:
1function openDatabase(name, version, upgradeCallback) {
2 return new Promise((resolve, reject) => {
3 const request = indexedDB.open(name, version);
4 request.onupgradeneeded = (event) => upgradeCallback(event.target.result);
5 request.onsuccess = () => resolve(request.result);
6 request.onerror = () => reject(request.error);
7 });
8}
9
10function getItem(db, storeName, key) {
11 return new Promise((resolve, reject) => {
12 const tx = db.transaction(storeName, 'readonly');
13 const store = tx.objectStore(storeName);
14 const request = store.get(key);
15 request.onsuccess = () => resolve(request.result);
16 request.onerror = () => reject(request.error);
17 });
18}
19
20function putItem(db, storeName, value) {
21 return new Promise((resolve, reject) => {
22 const tx = db.transaction(storeName, 'readwrite');
23 const store = tx.objectStore(storeName);
24 const request = store.put(value);
25 request.onsuccess = () => resolve(request.result);
26 request.onerror = () => reject(request.error);
27 });
28}
29
30function getAllItems(db, storeName) {
31 return new Promise((resolve, reject) => {
32 const tx = db.transaction(storeName, 'readonly');
33 const store = tx.objectStore(storeName);
34 const request = store.getAll();
35 request.onsuccess = () => resolve(request.result);
36 request.onerror = () => reject(request.error);
37 });
38}
Using async/await (Looks Synchronous)
1async function main() {
2 // Open database
3 const db = await openDatabase('myDB', 1, (db) => {
4 db.createObjectStore('users', { keyPath: 'id' });
5 });
6
7 // Write — reads like synchronous code
8 await putItem(db, 'users', { id: 1, name: 'Alice', age: 30 });
9 await putItem(db, 'users', { id: 2, name: 'Bob', age: 25 });
10
11 // Read
12 const user = await getItem(db, 'users', 1);
13 console.log(user); // { id: 1, name: 'Alice', age: 30 }
14
15 // Get all
16 const allUsers = await getAllItems(db, 'users');
17 console.log(allUsers); // [{ id: 1, ... }, { id: 2, ... }]
18}
19
20main();
This code is asynchronous under the hood but reads top-to-bottom like synchronous code.
Using the idb Library (Recommended)
The idb library by Jake Archibald wraps IndexedDB with Promises automatically:
1import { openDB } from 'idb';
2
3async function main() {
4 const db = await openDB('myDB', 1, {
5 upgrade(db) {
6 db.createObjectStore('users', { keyPath: 'id' });
7 },
8 });
9
10 // Write
11 await db.put('users', { id: 1, name: 'Alice', age: 30 });
12
13 // Read
14 const user = await db.get('users', 1);
15 console.log(user);
16
17 // Get all
18 const all = await db.getAll('users');
19 console.log(all);
20
21 // Transaction
22 const tx = db.transaction('users', 'readwrite');
23 await Promise.all([
24 tx.store.put({ id: 3, name: 'Charlie' }),
25 tx.store.put({ id: 4, name: 'Diana' }),
26 tx.done,
27 ]);
28}
Install with npm install idb. It is only 1.2KB gzipped.
Cursor Iteration with Promises
1function iterateStore(db, storeName, callback) {
2 return new Promise((resolve, reject) => {
3 const tx = db.transaction(storeName, 'readonly');
4 const store = tx.objectStore(storeName);
5 const request = store.openCursor();
6 const results = [];
7
8 request.onsuccess = (event) => {
9 const cursor = event.target.result;
10 if (cursor) {
11 results.push(callback(cursor.value));
12 cursor.continue();
13 } else {
14 resolve(results);
15 }
16 };
17 request.onerror = () => reject(request.error);
18 });
19}
20
21// Usage
22const names = await iterateStore(db, 'users', user => user.name);
23console.log(names); // ['Alice', 'Bob', 'Charlie']
Pre-Loading Data for Synchronous Access
If you need synchronous reads in a hot path, load data into memory first:
1class SyncCache {
2 constructor() {
3 this.cache = new Map();
4 }
5
6 async loadFromDB(db, storeName) {
7 const items = await getAllItems(db, storeName);
8 for (const item of items) {
9 this.cache.set(item.id, item);
10 }
11 }
12
13 // Synchronous read from memory cache
14 get(key) {
15 return this.cache.get(key);
16 }
17
18 // Async write-through to IndexedDB
19 async set(db, storeName, value) {
20 this.cache.set(value.id, value);
21 await putItem(db, storeName, value);
22 }
23}
24
25// Initialize once
26const cache = new SyncCache();
27await cache.loadFromDB(db, 'users');
28
29// Now reads are truly synchronous
30const user = cache.get(1); // No await needed
Common Pitfalls
Expecting synchronous access: No browser supports synchronous IndexedDB on the main thread. The IDBFactory.openSync() from the early spec was never implemented. Do not look for it.
Not awaiting transactions: IndexedDB transactions auto-commit when all requests are done. If you start a transaction and do async work before using it, the transaction may already be committed. Keep all operations within the same microtask.
Error handling: IndexedDB errors propagate via onerror callbacks, not exceptions. Without Promise wrappers, errors are silently lost. Always add error handlers or use the idb library.
Version management: indexedDB.open(name, version) only triggers onupgradeneeded when the version increases. Forgetting to bump the version means schema changes are silently ignored.
Blocking the page with large reads: Even async reads can cause jank if you process millions of records in a tight loop. Use cursors with requestAnimationFrame or Web Workers for large datasets.
Summary
IndexedDB has no synchronous API on the main thread — this is by design
Wrap IndexedDB requests in Promises and use async/await for synchronous-looking code
Use the idb library for a clean, Promise-based IndexedDB API
For truly synchronous access, pre-load data into a memory cache
Always handle errors — IndexedDB silently drops errors without proper handlers