Chapter 49: IndexedDB Client-Side Database

Promisifying IndexedDB & Modern Async/Await

Transforming legacy event-based request callbacks into ergonomic, robust `async`/`await` pipelines with zero-dependency wrappers and Jake Archibald's `idb` library.

LEARNING OBJECTIVES
  • Understand why IndexedDB was designed with IDBRequest event listeners instead of ES6 Promises.
  • Implement a lightweight, zero-dependency Promise wrapper for raw IDBRequest and IDBTransaction objects.
  • Integrate Jake Archibald's industry-standard idb library via modern ES modules.
  • Write expressive, linear database logic using async/await while managing transactional scopes.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

Imagine communicating with an embassy in the 1990s.

In the Raw Event API era, you filled out a paper application form (open()), walked over to the lobby, and sat down. Every single time something happened, a clerk would walk into the lobby and shout a status update into a megaphone (onsuccess, onerror, onblocked). If you needed to complete a four-step document workflow (open ➔ get record ➔ modify record ➔ verify update), you ended up writing deeply nested callback pyramids—the dreaded "Callback Hell."

Promisification is like giving that entire embassy process a modern smartphone app with push notifications and seamless sequential execution:

  • Instead of nesting callbacks four levels deep, you simply write const db = await openDB(...).
  • Instead of wiring request.onsuccess and request.onerror manually for every single read and write, you write await db.put('users', newUser).
  • Your code reads top-to-bottom like clean synchronous logic, while the browser runs non-blocking asynchronous disk I/O under the hood.

Technical Deep Dive & Specifications

Why Didn't IndexedDB Use Promises Originally?

IndexedDB was drafted in 2010–2011 and standardized in W3C IndexedDB 1.0 (2015). At that time, ECMAScript Promises were not yet part of the JavaScript language standard (standardized in ES6/ES2015). Consequently, the API was designed around the DOM Event Model (addEventListener, onsuccess, onerror).

Architecture of a Zero-Dependency Promise Wrapper

To bridge raw IndexedDB to async/await, we wrap IDBRequest and IDBTransaction in native Promises:

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

// Transaction completion promisifier
function promisifyTransaction(transaction) {
  return new Promise((resolve, reject) => {
    transaction.oncomplete = () => resolve();
    transaction.onabort = () => reject(transaction.error || new Error('Transaction aborted'));
    transaction.onerror = () => reject(transaction.error);
  });
}

The idb Library (Jake Archibald / Google Chrome Team)

The industry gold standard for modern IndexedDB development is the tiny (~1.2KB gzipped) idb library:

+-----------------------------------------------------------------------------+
|                                idb ARCHITECTURE                             |
+-----------------------------------------------------------------------------+
|                                                                             |
|   Modern ES6 App:  await db.get('users', 'u101')                            |
|                            │                                                |
|                            ▼                                                |
|   ┌──────────────────────────────────────────────────────────────────────┐  |
|   │                       `idb` Proxy & Promise Layer                    │  |
|   │  - Transparently manages transactions                                │  |
|   │  - Intercepts IDBRequest and wraps in Promise.resolve() / reject()   │  |
|   │  - Exposes modern db.openDB(), db.get(), db.put(), db.getAll()       │  |
|   └──────────────────────────────────────────────────────────────────────┘  |
|                            │                                                |
|                            ▼                                                |
|   Native Browser Engine: IDBDatabase / IDBTransaction / IDBRequest          |
|                                                                             |
+-----------------------------------------------------------------------------+

💻 Interactive Code Playground

Starter Code

Save this file as promisify.html and open it in your browser. This example imports idb directly via CDN as an ES Module.

Line-by-Line Code Breakdown

  • Line 60 (import { openDB } from 'https://cdn.jsdelivr.net/npm/idb@8/+esm'): Loads the modern ES module version of Jake Archibald's idb library.
  • Lines 72–80 (await openDB('ModernAppDB', 1, { upgrade(...) })): Modernized database initialization where schema migrations are declared cleanly inside the upgrade hook.
  • Line 90 (await db.put('users', user)): Convenience helper on the IDBDatabase wrapper that automatically opens a single-use readwrite transaction, executes store.put(), and returns a Promise.
  • Lines 111–125 (const tx = db.transaction(...); await tx.done;): Notice the await tx.done idiom. In idb, tx.done is a Promise that resolves when the underlying transaction commits successfully or rejects if it aborts.
  • Line 132 (await db.getAllFromIndex('users', 'by_tier', 'pro')): High-level helper that queries an index and returns an array of matching records in a single line of code.

Expected Browser Render Output


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...
[03:05:00] 🛠️ idb upgrade hook: Migrating 0 ➔ 1
[03:05:00] ✅ idb database initialized successfully!
[03:05:03] 💾 Saved user via await db.put()! Key: u_101
[03:05:05] ✨ User u_101 score updated to 500 seamlessly with async/await!
[03:05:08] 💸 Executing multi-store async transaction...
[03:05:08] 🎉 Transaction committed! tx.done resolved.

🏋️ Hands-On Exercise

🎯 The Challenge: Build a Zero-Dependency IDB Promise Utility

Instructions:

  1. Without importing any external libraries, create your own minimal async wrapper function: async function getIDB(dbName, version, upgradeCallback).
  2. Implement two helper methods on the returned database wrapper:
    • get(storeName, key): Returns a Promise resolving to the record.
    • set(storeName, value): Returns a Promise resolving when the record is saved.
  3. Test your custom utility by saving and reading { id: 'config_1', theme: 'dark', fontSize: 16 }.

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Awaiting Non-IDB Promises Inside Transactions: Even when using idb, awaiting a non-IDB Promise (e.g. await fetch() or await new Promise(...)) inside a transaction will cause the transaction to auto-commit and fail with TransactionInactiveError.
  2. Forgetting to Await tx.done: In idb, initiating a write operation on a transaction returns immediately. Always await tx.done before concluding the workflow to ensure data has been flushed to disk.
  3. Uncaught Rejections on Quota Exceeded: When disk quota is exceeded, await db.put() rejects with a QuotaExceededError. Always enclose write calls in try / catch blocks.

💡 Pro Tips

  1. Leverage idb's Key-Value Helper Shortcut: For simple storage needs that require IndexedDB's quota and binary support without full relational queries, idb/keyval offers get(key), set(key, val), and del(key) in a tiny 600-byte footprint.
  2. Type Safety with TypeScript: Jake Archibald's idb has first-class TypeScript support (openDB<MyDB>('name', 1)), providing strict compile-time autocomplete for store names, indexes, and record types.

📌 Key Takeaways

  • IndexedDB originally used IDBRequest events because it predated native ES6 Promises.
  • Wrapping IDBRequest and IDBTransaction in native Promises enables clean async/await workflows.
  • Jake Archibald's idb library is the industry standard wrapper, providing high-level helpers like db.get(), db.put(), and db.getAllFromIndex().
  • The await tx.done promise guarantees that all operations in a transaction have successfully committed to disk.
  • Even in promisified IndexedDB, introducing non-IDB async delays inside an open transaction will trigger TransactionInactiveError.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Why must you await tx.done when executing a multi-step transaction using the idb library?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

What happens if you execute await fetch('/data.json') between two await store.put() calls inside an active idb transaction?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

In the idb library's openDB(name, version, options) method, what is the name of the lifecycle hook where ObjectStores and Indexes must be created?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP