Skip to content

HTML5 - IndexedDB

This tutorial explores IndexedDB, the standard API for robust client-side storage of significant amounts of structured data. It allows web applications to store data persistently within the user’s browser.

Note: The older Web SQL Database API, previously covered here, is now deprecated and should not be used in new projects. It was never formally adopted as a web standard and has been removed from or is unsupported by most modern browsers. IndexedDB is the recommended modern alternative.

IndexedDB provides an asynchronous, transactional, object-oriented database. Instead of tables with rows and columns like SQL databases, it uses object stores containing key-value pairs. It is supported by all modern web browsers.

Understanding these core concepts is key to working with IndexedDB:

  • Database: A container for your data. You open a connection to a specific database by name and version.
  • Object Store: Similar to tables in SQL databases. Each object store holds records (JavaScript objects) identified by a key.
  • Index: Allows efficient lookup of records based on properties other than the primary key. Like indexes in SQL.
  • Transaction: A wrapper around operations ensuring data integrity (atomicity). All reads and writes happen within transactions.
  • Request: IndexedDB operations are asynchronous. They return request objects with onsuccess and onerror event handlers to manage results or errors. Modern development often uses Promises to handle these asynchronous operations more easily.

The first step is to request opening a connection to a database using indexedDB.open(). This method takes the database name and an optional version number.

// Request to open the database named 'myAppDB' with version 1
const request = indexedDB.open('myAppDB', 1);

Opening a database is an asynchronous operation. You need event handlers to know when it succeeds, fails, or needs a schema upgrade:

  • onerror: Handles errors during the open request.
  • onsuccess: Fires when the connection is successfully established. The database connection object is available in event.target.result.
  • onupgradeneeded: Fires if the database doesn’t exist yet or if a higher version number is requested than the existing one. This is the only place where you can create or modify the database schema (object stores and indexes).
let db;
const request = indexedDB.open('myNotesAppDB', 1);
request.onerror = (event) => {
console.error(`Database error: ${event.target.errorCode}`);
};
request.onsuccess = (event) => {
db = event.target.result;
console.log(`Database opened successfully.`);
// You can start using the database connection (db) here
};
request.onupgradeneeded = (event) => {
db = event.target.result;
console.log(`Database upgrade needed or initial setup.`);
// Create an object store called 'notes'
// 'id' will be the key, and it will auto-increment
const objectStore = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
// Create an index named 'title_idx' on the 'title' property
objectStore.createIndex('title_idx', 'title', { unique: false });
// Create an index named 'created_idx' on the 'created' property
objectStore.createIndex('created_idx', 'created', { unique: false });
console.log('Object store and indexes created.');
};

Using Promises can significantly simplify handling asynchronous operations. Libraries like idb (by Jake Archibald) provide a thin promise-based wrapper around IndexedDB, but here’s how you might wrap the open request manually:

function openNotesDatabase(dbName, version) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName, version);
request.onerror = (event) => {
reject(`Database error: ${event.target.errorCode}`);
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onupgradeneeded = (event) => {
console.log(`Database upgrade needed for version ${version}.`);
const db = event.target.result;
if (!db.objectStoreNames.contains('notes')) {
const objectStore = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
objectStore.createIndex('title_idx', 'title', { unique: false });
objectStore.createIndex('created_idx', 'created', { unique: false });
console.log('Object store \'notes\' created.');
}
// Add more upgrade logic for future versions here if needed
};
});
}
// Usage:
openNotesDatabase('myNotesAppDB', 1)
.then(dbInstance => {
console.log('Database opened successfully via Promise');
db = dbInstance; // Assign to global or module-level variable if needed
// Start using the dbInstance
})
.catch(error => {
console.error(error);
});

All data operations (Create, Read, Update, Delete) must happen within a transaction.

  1. Start a transaction: Specify the object store(s) involved and the mode ('readonly' or 'readwrite').

  2. Get the object store: Access the desired object store from the transaction object.

  3. Perform the operation: Use methods like add(), put(), get(), getAll(), delete(), or openCursor().

  4. Handle results: Use onsuccess and onerror on the request object returned by the operation, or better yet, use Promises.

Use the add() method to add a new object to the store. It fails if an object with the same key already exists. Use put() to add or update (overwrite) an object.

function addNote(db, note) {
return new Promise((resolve, reject) => {
// Start a read-write transaction
const transaction = db.transaction(['notes'], 'readwrite');
const objectStore = transaction.objectStore('notes');
// Add the note object
const request = objectStore.add(note);
request.onsuccess = (event) => {
// event.target.result contains the key of the added item
resolve(event.target.result);
};
request.onerror = (event) => {
reject(`Error adding note: ${event.target.errorCode}`);
};
// Optional: Handle transaction completion/abortion
transaction.oncomplete = () => {
console.log('Add transaction completed.');
};
transaction.onerror = (event) => {
console.error(`Transaction error: ${event.target.errorCode}`);
// Note: Request errors often bubble up to transaction errors too
};
});
}
// Usage (assuming 'db' is the opened database connection):
const newNote = {
title: 'My First Note',
body: 'This is the content of the note.',
created: new Date()
};
addNote(db, newNote)
.then(addedKey => {
console.log(`Note added with key: ${addedKey}`);
})
.catch(error => {
console.error(error);
});

Use get(key) to retrieve a single object by its key, or getAll() to retrieve all objects in the store. You can also use indexes and cursors for more complex queries.

function getAllNotes(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['notes'], 'readonly');
const objectStore = transaction.objectStore('notes');
// Get all notes
const request = objectStore.getAll();
request.onsuccess = (event) => {
// event.target.result contains an array of all note objects
resolve(event.target.result);
};
request.onerror = (event) => {
reject(`Error getting notes: ${event.target.errorCode}`);
};
});
}
// Usage:
getAllNotes(db)
.then(notes => {
console.log('Retrieved notes:', notes);
// Update UI with the notes
const statusDiv = document.getElementById('status'); // Assuming an element with id='status'
statusDiv.innerHTML = `<p>Found ${notes.length} notes.</p>`;
notes.forEach(note => {
statusDiv.innerHTML += `<p>**${note.title}**: ${note.body}</p>`;
});
})
.catch(error => {
console.error(error);
});

Use the put() method. If an object with the specified key exists, it’s updated; otherwise, a new object is added.

function updateNote(db, note) { // note object must include the 'id' (keyPath)
return new Promise((resolve, reject) => {
const transaction = db.transaction(['notes'], 'readwrite');
const objectStore = transaction.objectStore('notes');
const request = objectStore.put(note); // Use put for update
request.onsuccess = (event) => {
// event.target.result contains the key of the updated/added item
resolve(event.target.result);
};
request.onerror = (event) => {
reject(`Error updating note: ${event.target.errorCode}`);
};
});
}
// Usage (assuming you have a note with id=1 retrieved earlier):
const noteToUpdate = {
id: 1, // Must match the existing key
title: 'My Updated Note Title',
body: 'The content has been updated.',
created: new Date() // Or keep the original date
};
updateNote(db, noteToUpdate)
.then(updatedKey => {
console.log(`Note updated with key: ${updatedKey}`);
})
.catch(error => {
console.error(error);
});

Use the delete(key) method to remove an object by its key.

function deleteNote(db, key) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['notes'], 'readwrite');
const objectStore = transaction.objectStore('notes');
const request = objectStore.delete(key);
request.onsuccess = (event) => {
// Success indicates the delete operation was queued
resolve(`Note with key ${key} delete request successful.`);
};
request.onerror = (event) => {
reject(`Error deleting note: ${event.target.errorCode}`);
};
// It's often better to resolve/reject on transaction completion
transaction.oncomplete = () => {
console.log(`Delete transaction for key ${key} completed.`);
resolve(`Note with key ${key} successfully deleted.`); // Resolve here for certainty
};
transaction.onerror = (event) => {
reject(`Delete transaction error: ${event.target.errorCode}`);
};
});
}
// Usage (deleting the note with id=2):
deleteNote(db, 2)
.then(message => {
console.log(message);
})
.catch(error => {
console.error(error);
});

Here’s a basic HTML page demonstrating opening the database, adding a note, and then reading all notes, using Promises for cleaner async handling.

<!DOCTYPE HTML>
<html>
<head>
<title>IndexedDB Notes App</title>
<meta charset="UTF-8">
</head>
<body>
<h1>My Notes</h1>
<div id="status" name="status">
Loading database...
</div>
<div id="notesList">
<!-- Notes will be displayed here -->
</div>
<script>
let db; // Variable to hold the database connection
const dbName = 'myNotesAppDB';
const dbVersion = 1;
// --- Promise-based Helper Functions ---
function openNotesDatabase(name, version) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onerror = event => reject(`Database error: ${event.target.errorCode}`);
request.onsuccess = event => resolve(event.target.result);
request.onupgradeneeded = event => {
console.log(`Upgrading database to version ${version}...`);
const tempDb = event.target.result;
if (!tempDb.objectStoreNames.contains('notes')) {
const store = tempDb.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
store.createIndex('title_idx', 'title', { unique: false });
console.log('Object store \'notes\' created.');
}
};
});
}
function performTransaction(db, storeName, mode, action) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], mode);
const objectStore = transaction.objectStore(storeName);
transaction.oncomplete = () => resolve(); // Transaction succeeded
transaction.onerror = event => reject(`Transaction error: ${event.target.errorCode}`);
try {
action(objectStore, resolve, reject); // Pass store and promise controls to the action
} catch (err) {
reject(err);
transaction.abort(); // Abort transaction on action error
}
});
}
function addNote(db, note) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['notes'], 'readwrite');
const store = transaction.objectStore('notes');
const request = store.add(note);
request.onsuccess = event => resolve(event.target.result); // Resolve with the new key
request.onerror = event => reject(`Add error: ${event.target.error}`);
transaction.onerror = event => reject(`Transaction error: ${event.target.error}`);
});
}
function getAllNotes(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['notes'], 'readonly');
const store = transaction.objectStore('notes');
const request = store.getAll();
request.onsuccess = event => resolve(event.target.result); // Resolve with the array of notes
request.onerror = event => reject(`GetAll error: ${event.target.error}`);
transaction.onerror = event => reject(`Transaction error: ${event.target.error}`);
});
}
function displayNotes(notes) {
const listDiv = document.getElementById('notesList');
const statusDiv = document.getElementById('status');
listDiv.innerHTML = ''; // Clear previous list
if (notes.length === 0) {
statusDiv.textContent = 'No notes stored yet.';
return;
}
statusDiv.textContent = `Displaying ${notes.length} notes:`;
const ul = document.createElement('ul');
notes.forEach(note => {
const li = document.createElement('li');
li.textContent = `ID ${note.id}: ${note.title} - ${note.body} (Created: ${new Date(note.created).toLocaleString()})`;
ul.appendChild(li);
});
listDiv.appendChild(ul);
}
// --- Main Logic ---
async function initializeApp() {
try {
db = await openNotesDatabase(dbName, dbVersion);
document.getElementById('status').textContent = 'Database opened successfully.';
// Example: Add a note if none exist (simple check)
const currentNotes = await getAllNotes(db);
if (currentNotes.length === 0) {
console.log('Adding initial notes...');
const key1 = await addNote(db, { title: 'Welcome', body: 'This is your first note!', created: new Date() });
const key2 = await addNote(db, { title: 'Reminder', body: 'Learn IndexedDB', created: new Date() });
console.log(`Added notes with keys: ${key1}, ${key2}`);
}
// Load and display all notes
const notes = await getAllNotes(db);
displayNotes(notes);
} catch (error) {
console.error('Initialization failed:', error);
document.getElementById('status').textContent = `Error: ${error}`;
}
}
// Start the application
initializeApp();
</script>
</body>
</html>
  • Asynchronous Operations: Always remember IndexedDB is asynchronous. Use callbacks, Promises (async/await), or libraries like idb to manage this.
  • Error Handling: Implement onerror handlers for requests and transactions. Errors can occur due to various reasons (quota exceeded, constraints violated, etc.).
  • Transaction Scope: Keep transactions short and specific to the task. Long-running readwrite transactions can lock the database.
  • Schema Versioning: Plan your schema upgrades carefully using the onupgradeneeded event and incrementing the database version.
  • Browser Compatibility: While widely supported, always check specific features or limits if targeting older browsers. caniuse.com is a good resource.
  • Storage Limits: Browsers impose storage limits, which can vary. Handle potential quota errors gracefully.
  • Performance: Use indexes for efficient querying on large datasets. Avoid retrieving large amounts of unnecessary data.

For more in-depth information, consult the official documentation and related guides: