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
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?