Iterating with Cursors and Ranges
What is a cursor?
Section titled “What is a cursor?”Reading a single record by key with objectStore.get(key) is fast, but sometimes you need to walk through many records — filtering, aggregating, or transforming as you go. That is what cursors are for.
A cursor points at one record at a time. You open it on an object store (or index), process the current record, then call cursor.continue() to advance to the next one. When there are no more records the cursor resolves to null, signalling that iteration is complete.
const request = objectStore.openCursor();
request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { console.log(cursor.key, cursor.value); cursor.continue(); // advance to next record } else { console.log('Done — no more records'); }};Opening a cursor
Section titled “Opening a cursor”objectStore.openCursor(query, direction) — both arguments are optional.
query— anIDBKeyRangeor a specific key. Omit (or passnull) to iterate all records.direction—"next"(default, ascending),"prev"(descending),"nextunique", or"prevunique".
// All records, ascending order (default)objectStore.openCursor();
// All records, descending orderobjectStore.openCursor(null, 'prev');
// Only records with keys 2–3 (inclusive on both ends)objectStore.openCursor(IDBKeyRange.bound(2, 3));IDBKeyRange — filtering by key
Section titled “IDBKeyRange — filtering by key”IDBKeyRange lets you describe a subset of keys without scanning every record. The four factory methods are:
| Method | What it matches |
|---|---|
IDBKeyRange.only(value) | Exactly one key |
IDBKeyRange.lowerBound(lower, open?) | Keys ≥ lower (pass true to exclude lower itself) |
IDBKeyRange.upperBound(upper, open?) | Keys ≤ upper (pass true to exclude upper itself) |
IDBKeyRange.bound(lower, upper, lowerOpen?, upperOpen?) | Keys between lower and upper, inclusive by default |
IDBKeyRange.only(42) // key === 42IDBKeyRange.lowerBound(10) // key >= 10IDBKeyRange.lowerBound(10, true) // key > 10IDBKeyRange.upperBound(50) // key <= 50IDBKeyRange.bound(10, 50) // 10 <= key <= 50IDBKeyRange.bound(10, 50, true, true) // 10 < key < 50Wrapping cursor iteration in a Promise
Section titled “Wrapping cursor iteration in a Promise”Raw onsuccess callbacks are tedious. Wrapping the pattern in a helper makes it composable with async/await:
function iterateCursor(store, range = null) { return new Promise((resolve, reject) => { const results = []; const req = store.openCursor(range); req.onsuccess = (e) => { const cursor = e.target.result; if (cursor) { results.push(cursor.value); cursor.continue(); } else { resolve(results); } }; req.onerror = () => reject(req.error); });}Runnable: iterate all records, then a range
Section titled “Runnable: iterate all records, then a range”The demo below:
- Opens a database and seeds four player score records.
- Uses a cursor to iterate all records and logs each one.
- Uses
IDBKeyRange.bound(2, 3)to iterate only the records withid2 and 3. - Deletes the database afterwards to leave no trace.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
A cursor + IDBKeyRange instead of getAll() | Processes one record at a time without loading everything into memory at once — good for a large dataset | More complex code — you handle callbacks and call continue() yourself every time |
Calling getAll() directly | Short, readable code, giving you an array right away | Loads every record into memory in one shot — unsuitable for a large store |
Filtering with IDBKeyRange before iterating | Fewer records to scan, saving time | You need to know the key structure in advance to build the correct range |
Common mistakes
Section titled “Common mistakes”- Forgetting to call
cursor.continue()inside theonsuccesshandler — iteration just stops silently, with no error to see. Call it every time records remain. - Assuming
IDBKeyRange.bound()excludes both edges by default — the default is inclusive of both the lower and upper bound. Passtrueexplicitly as the third or fourth argument if you want it exclusive. - Opening a new cursor every time you want the next record — a single cursor can keep advancing with
continue(); there’s no need to reopen it. Doing so wastes performance and confuses the code.