IndexedDB

Write and read structured notes

Store real JavaScript values with put(), retrieve them by key, and distinguish insert-only behavior from replacement.

IndexedDB stores JavaScript values, not strings. That’s the first big difference from Web Storage, and it changes how you write your records.

Structured clone

When you save an object, the browser copies it with the structured clone algorithm, the same mechanism postMessage() uses. It handles nested objects, arrays, Date, Map, Set, typed arrays, and Blob. Nothing gets turned into JSON.

So a note can carry a real Date, and it comes back as a Date:

const note = {
  id: crypto.randomUUID(),
  title: 'Trail conditions',
  body: 'The north path is wet',
  updatedAt: new Date(),
}

await db.put('notes', note)
const saved = await db.get('notes', note.id)

saved.updatedAt instanceof Date is true. With localStorage you’d get a string and have to parse it back yourself.

crypto.randomUUID() gives us a stable primary key, like 3b2f7d1e-8a4c-4f0e-9b6d-2c1a5e7f9d3a. Since we declared keyPath: 'id', IndexedDB reads the key from the record itself.

add() versus put()

Both write a record. The difference is what happens when the key already exists.

add() is insert-only. If a record with the same key is already in the store, the request fails with a ConstraintError and the transaction aborts. Use it when a duplicate means something went wrong.

put() inserts or replaces. Same key, new content, no error. That’s what we want when the user edits a note and saves it again.

Try it in the console:

await db.add('notes', note) //ConstraintError: Key already exists in the object store.

Reading one record

get() takes the store name and a key. When the key doesn’t exist, it resolves to undefined, not an error. Handle that, or your code throws a TypeError the first time a user opens a deleted note:

const note = await db.get('notes', id)
if (!note) {
  showMessage('This note no longer exists')
  return
}

What you can’t store

The structured clone algorithm refuses functions, DOM elements, and class instances with methods. Try to put() a note that holds a reference to its <textarea> and you get a DataCloneError.

The fix is a habit: store data, not live objects. Keep the note as plain fields and reconnect it to the interface when you load it.

Two habits that save trouble

Don’t call getAll() for the notes list once the collection grows. It loads every record into memory. Ten notes, fine. Five thousand, not fine. The next lesson adds an index and a bound so we load only what the screen shows.

And keep the id stable across edits. If your save handler generates a fresh UUID each time, put() sees a new key and creates a second note. The user now has two “Trail conditions” and the old one never updates. Generate the ID once, when the note is created, and carry it along.

Add three notes, replace one with put(), then try the same key with add(). Look at the returned record in the first case and the ConstraintError in the second, and check the store in DevTools still has exactly three rows.

Lesson completed