IndexedDB is a low-level browser API for storing large amounts of structured data on the client. After you insert records, your app needs to retrieve them—to populate lists, restore drafts, or sync offline caches when the user returns.
This tutorial focuses specifically on reading data from IndexedDB: fetching one record with get(), loading everything with getAll(), walking rows with openCursor(), and handling missing keys gracefully.
What You’ll Learn
01
get()
One by key.
02
getAll()
Every row.
03
openCursor
Iterate.
04
Indexes
Query fields.
05
readonly
Tx mode.
06
undefined
Not found.
Fundamentals
What Is IndexedDB?
IndexedDB is a transactional, key-value database built into modern browsers. Data lives in object stores as plain JavaScript objects. The API is asynchronous—reads use requests and callbacks (or Promises if you wrap them), so the main thread stays responsive during large fetches.
Reads never block the UI like a synchronous SQL query might. That makes IndexedDB suitable for offline apps, cached catalogs, and progressive web apps that store thousands of records locally.
💡
Beginner Tip
Think of get(key) like looking up one row by id. Think of openCursor() like walking through the table one row at a time—great when you do not know every key upfront.
Foundation
Setting Up IndexedDB
Before retrieving data, open the database and ensure your object store (and optional indexes) exist:
js
let db;
const request = indexedDB.open('myDatabase', 1);
request.onupgradeneeded = function (event) {
db = event.target.result;
const store = db.createObjectStore('myStore', { keyPath: 'id' });
store.createIndex('name', 'name', { unique: false });
};
request.onsuccess = function (event) {
db = event.target.result;
/* db is ready — start readonly transactions here */
};
request.onerror = function (event) {
console.error('Open failed:', event.target.error.message);
};
Schema setup runs only in onupgradeneeded. After that, every page load opens the existing database and you can read immediately in onsuccess.
Core Topic
Retrieving Data from IndexedDB
To fetch one record by its primary key, use get() inside a readonly transaction:
js
const tx = db.transaction(['myStore'], 'readonly');
const store = tx.objectStore('myStore');
const request = store.get(1); /* keyPath id = 1 */
request.onsuccess = function () {
const data = request.result;
if (data) {
console.log('Data retrieved:', data);
} else {
console.warn('No record with id 1');
}
};
request.onerror = function (event) {
console.error('Get failed:', event.target.error.message);
};
For small stores where you need every record at once, modern browsers support getAll():
Cursors walk through records one at a time. Use them when the dataset is large, you need to filter in JavaScript, or you want to process rows without loading all into memory:
js
const tx = db.transaction(['myStore'], 'readonly');
const store = tx.objectStore('myStore');
const request = store.openCursor();
request.onsuccess = function (event) {
const cursor = event.target.result;
if (cursor) {
console.log('Key:', cursor.key, 'Value:', cursor.value);
cursor.continue(); /* next record */
} else {
console.log('No more entries');
}
};
request.onerror = function (event) {
console.error('Cursor error:', event.target.error.message);
};
You can also open a cursor on an index to iterate records sorted by name or filtered by an indexed field:
js
const index = store.index('name');
const req = index.openCursor();
req.onsuccess = function (event) {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.value.name, cursor.value.email);
cursor.continue();
}
};
Reliability
Handling Errors
Distinguish between a real error and a missing record. get() on a non-existent key succeeds with result === undefined—that is not a failure:
js
function getContact(db, id) {
return new Promise(function (resolve, reject) {
const tx = db.transaction(['contacts'], 'readonly');
const req = tx.objectStore('contacts').get(id);
req.onsuccess = function () {
resolve(req.result); /* may be undefined */
};
req.onerror = function () {
reject(req.error);
};
tx.onerror = function () {
reject(tx.error);
};
});
}
getContact(db, 42)
.then(function (record) {
if (!record) {
console.warn('Contact 42 not found');
} else {
console.log(record.name);
}
})
.catch(function (err) {
console.error('Read error:', err.message);
});
Always use error.message in callbacks, not the legacy errorCode property from older tutorials.
Cheat Sheet
⚡ Quick Reference
Operation
Code pattern
Get one record
store.get(key)
Get all records
store.getAll()
Iterate records
store.openCursor() + continue()
Query by index
store.index('name').get(value)
Count records
store.count()
Read transaction
db.transaction(['store'], 'readonly')
One row
get(key)
By id
All rows
getAll()
Array
Walk
openCursor()
Iterate
Missing
if (result)
Check
Hands-On
Examples Gallery
These examples demonstrate IndexedDB read patterns. Use Try It Yourself to run live demos in the editor.
📚 Getting Started
Fetch a single record by its primary key.
Example 1 — get() by Primary Key
Load the contact with id = 1 from the contacts store.
The page seeds id 1 on first open. Clicking Retrieve runs get(1) in a readonly transaction and displays the JSON string—no developer console needed.
Applications
🚀 Common Use Cases
Restore drafts — get(draftId) when the user reopens a form.
Populate lists — getAll() or cursors to fill todo or message lists offline.
Login by email — index lookup to find a cached user profile.
Sync status — read queued items before sending to a server.
Verify before delete — get() first to show what will be removed.
🧠 How IndexedDB Retrieve Works
1
readonly tx
db.transaction(['store'], 'readonly')
Open
2
get / cursor
Request the record(s) you need.
Query
3
onsuccess
Read request.result in the callback.
Result
=
🔍
Data in hand
Render in the UI or pass to your app logic.
Important
📝 Notes
Use readonly transactions for all read operations.
get() returning undefined means “not found”—not an error.
getAll() loads every row; use cursors for very large stores.
Indexes must be created in onupgradeneeded before you can query them.
All requests in a transaction must complete before the transaction finishes.
After reading, continue to Update or Delete as needed.
Compatibility
Universal Browser Support
IndexedDB get(), getAll(), openCursor(), and indexes are supported in all modern browsers: Chrome, Firefox, Safari, Edge, and mobile WebViews.
✓ Baseline · Since HTML
IndexedDB read API
IndexedDB get(), getAll(), openCursor(), and indexes are supported in all modern browsers: Chrome, Firefox, Safari, Edge, and mobile WebViews.
95%Modern browser support
Google ChromeAll versions · Desktop & Mobile
Full support
Mozilla FirefoxAll versions · Desktop & Mobile
Full support
Apple SafariAll versions · macOS & iOS
Full support
Microsoft EdgeAll versions · Chromium & Legacy
Full support
Internet ExplorerIE 6+ · Legacy environments
Full support
OperaAll modern versions
Full support
IndexedDB read APIExcellent
Bottom line: Safe to use IndexedDB reads in production cross-browser web apps.
Pro Tips
💡 Best Practices
✅ Do
Use readonly transactions for reads
Check if (result) after every get()
Use indexes for lookups on non-key fields
Prefer cursors for large datasets
Wrap callbacks in Promises for cleaner async code
Handle onerror on requests and transactions
❌ Don’t
Assume get() always returns an object
Use getAll() on huge stores without testing memory
Query indexes that were never created in upgrade
Block the UI waiting synchronously for IndexedDB
Use readwrite when you only need to read
Forget to call cursor.continue() in loops
Wrap Up
Conclusion
Retrieving data from IndexedDB means opening a readonly transaction and using get(), getAll(), or openCursor() to read records. Check for undefined when a key is missing, and use indexes when you need to search by fields other than the primary key.
With these patterns you can build offline lists, restore user data, and verify records before updating or deleting them. Next, learn how to update existing records with put().
Use these points when reading data from a client-side database.
5
Core concepts
🔍01
get()
One key.
Basics
📄02
getAll()
All rows.
Batch
🔄03
Cursor
Iterate.
Scale
🔒04
readonly
Tx mode.
Reads
❓05
undefined
Not found.
Check
❓ Frequently Asked Questions
Open the database with indexedDB.open(), start a readonly transaction: db.transaction(['storeName'], 'readonly'), get the object store, then call store.get(key) for one record or store.getAll() for every record. Handle the result in the request's onsuccess callback.
get(key) returns one record by primary key instantly. openCursor() lets you iterate records one at a time—useful for large datasets, filtering in JavaScript, or walking an index without loading everything into memory at once.
When no record matches the key, get() succeeds but result is undefined. That is not an error—check if (result) before using the data and show a friendly 'not found' message in your UI.
Yes. Use db.transaction(['store'], 'readonly') for get(), getAll(), openCursor(), and count(). You can omit the mode (defaults to readonly) but being explicit is clearer for beginners.
Create an index in onupgradeneeded with createIndex(), then use store.index('indexName').get(value) or openCursor() on that index to look up records by email, name, or other fields.
Yes. get(), getAll(), openCursor(), and indexes work in Chrome, Firefox, Safari, Edge, and mobile browsers. IndexedDB is the standard API for client-side structured storage.
Did you know?
If you omit the transaction mode, IndexedDB defaults to readonly—so db.transaction(['store']) is valid for reads. Being explicit with 'readonly' makes your intent clearer to other developers reviewing the code.