Skip to content

Iterating with Cursors and Ranges

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');
}
};

objectStore.openCursor(query, direction) — both arguments are optional.

  • query — an IDBKeyRange or a specific key. Omit (or pass null) to iterate all records.
  • direction — "next" (default, ascending), "prev" (descending), "nextunique", or "prevunique".
// All records, ascending order (default)
objectStore.openCursor();
// All records, descending order
objectStore.openCursor(null, 'prev');
// Only records with keys 2–3 (inclusive on both ends)
objectStore.openCursor(IDBKeyRange.bound(2, 3));

IDBKeyRange lets you describe a subset of keys without scanning every record. The four factory methods are:

MethodWhat 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 === 42
IDBKeyRange.lowerBound(10) // key >= 10
IDBKeyRange.lowerBound(10, true) // key > 10
IDBKeyRange.upperBound(50) // key <= 50
IDBKeyRange.bound(10, 50) // 10 <= key <= 50
IDBKeyRange.bound(10, 50, true, true) // 10 < key < 50

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:

  1. Opens a database and seeds four player score records.
  2. Uses a cursor to iterate all records and logs each one.
  3. Uses IDBKeyRange.bound(2, 3) to iterate only the records with id 2 and 3.
  4. Deletes the database afterwards to leave no trace.
Browser Storage
OptionBenefitCost
A cursor + IDBKeyRange instead of getAll()Processes one record at a time without loading everything into memory at once — good for a large datasetMore complex code — you handle callbacks and call continue() yourself every time
Calling getAll() directlyShort, readable code, giving you an array right awayLoads every record into memory in one shot — unsuitable for a large store
Filtering with IDBKeyRange before iteratingFewer records to scan, saving timeYou need to know the key structure in advance to build the correct range
  • Forgetting to call cursor.continue() inside the onsuccess handler — 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. Pass true explicitly 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.
What value does the cursor resolve to when there are no more records to iterate?
Which IDBKeyRange factory creates a range that includes keys from 5 to 10, inclusive on both ends?
What happens if you forget to call cursor.continue() inside the onsuccess handler?
You want to iterate only records whose key is strictly greater than 10. Which range is correct?