A minimalistic wrapper for IndexedDB providing reactive queries, transactions, and schema management
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Comprehensive guide to Dexie's error system, error types, and handling strategies for robust database operations.
Dexie provides a sophisticated error handling system built around the DexieError base class and specialized error types for different database operations. The system includes:
DexieErrorThe foundation of Dexie's error system is the DexieError class:
/**
* Common base class for all errors originating from Dexie.js except TypeError,
* SyntaxError and RangeError.
*/
interface DexieError extends Error {
name: string;
message: string;
stack: string;
inner: any;
toString(): string;
}
interface DexieErrorConstructor {
new(msg?: string, inner?: Object): DexieError;
new(inner: Object): DexieError;
prototype: DexieError;
}Database lifecycle and schema errors:
/**
* Database operation and lifecycle error types
*/
type DexieErrors = {
// Database Opening and Connection
OpenFailed: 'OpenFailedError';
VersionChange: 'VersionChangeError';
DatabaseClosed: 'DatabaseClosedError';
MissingAPI: 'MissingAPIError';
NoSuchDatabase: 'NoSuchDatabaseError';
// Schema and Structure
Schema: 'SchemaError';
Upgrade: 'UpgradeError';
InvalidTable: 'InvalidTableError';
// Transaction Management
SubTransaction: 'SubTransactionError';
TransactionInactive: 'TransactionInactiveError';
PrematureCommit: 'PrematureCommitError';
ForeignAwait: 'ForeignAwaitError';
// Data Operations
InvalidArgument: 'InvalidArgumentError';
ReadOnly: 'ReadOnlyError';
// System Errors
Unsupported: 'UnsupportedError';
Internal: 'InternalError';
}Errors mapped from underlying IndexedDB operations:
/**
* IndexedDB errors mapped to Dexie error types
*/
type IndexedDBErrors = {
// Data Integrity
Constraint: 'ConstraintError';
Data: 'DataError';
DataClone: 'DataCloneError';
// Access and State
InvalidState: 'InvalidStateError';
InvalidAccess: 'InvalidAccessError';
NotFound: 'NotFoundError';
Version: 'VersionError';
// Transaction and Storage
Abort: 'AbortError';
Timeout: 'TimeoutError';
QuotaExceeded: 'QuotaExceededError';
// General
Unknown: 'UnknownError';
}Thrown during bulk modify operations with detailed failure information:
/**
* Error thrown during Collection.modify() operations containing
* information about which operations succeeded and failed
*/
interface ModifyError extends DexieError {
failures: Array<any>;
failedKeys: IndexableTypeArrayReadonly;
successCount: number;
}
interface ModifyErrorConstructor {
new (
msg?: string,
failures?: any[],
successCount?: number,
failedKeys?: IndexableTypeArrayReadonly
): ModifyError;
prototype: ModifyError;
}Thrown during bulk operations like bulkAdd, bulkPut, or bulkDelete:
/**
* Error thrown during bulk operations containing information
* about which specific operations failed
*/
interface BulkError extends DexieError {
failures: Error[];
failuresByPos: {[operationNumber: number]: Error};
}
interface BulkErrorConstructor {
new (
msg?: string,
failures?: {[operationNumber: number]: Error}
): BulkError;
prototype: BulkError;
}Standard promise-based error handling:
import Dexie from 'dexie';
const db = new Dexie('MyDatabase');
db.version(1).stores({
users: '++id, name, email'
});
// Basic error handling
try {
await db.users.add({ name: 'John', email: 'john@example.com' });
} catch (error) {
if (error instanceof Dexie.ConstraintError) {
console.log('Constraint violation:', error.message);
} else if (error instanceof Dexie.DatabaseClosedError) {
console.log('Database is closed');
} else {
console.log('Unexpected error:', error);
}
}Dexie promises support enhanced catch methods with error type filtering:
// Catch specific error types
db.users.add({ name: 'John', email: 'existing@email.com' })
.catch('ConstraintError', error => {
console.log('Email already exists');
// Only catch ConstraintError, others bubble up
})
.catch(Dexie.InvalidArgumentError, error => {
console.log('Invalid data provided');
// Catch by constructor
})
.catch(error => {
console.log('Other error:', error);
// Catch all remaining errors
});Transactions automatically roll back on any error:
// Transaction with error handling
try {
await db.transaction('rw', [db.users, db.orders], async () => {
const userId = await db.users.add({
name: 'John',
email: 'john@example.com'
});
await db.orders.add({
userId: userId,
product: 'Widget',
amount: 99.99
});
// If any operation fails, entire transaction rolls back
});
} catch (error) {
if (error instanceof Dexie.ConstraintError) {
console.log('Data constraint violation - transaction rolled back');
} else if (error instanceof Dexie.TransactionInactiveError) {
console.log('Transaction became inactive');
} else {
console.log('Transaction failed:', error.message);
}
}Handle partial failures in bulk operations:
const users = [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'alice@example.com' }, // Duplicate email
{ name: 'Charlie', email: 'charlie@example.com' }
];
try {
await db.users.bulkAdd(users);
} catch (error) {
if (error instanceof Dexie.BulkError) {
console.log(`${error.failures.length} operations failed`);
// Check specific failures
error.failures.forEach((failure, index) => {
console.log(`Item ${index} failed:`, failure.message);
});
// Access failures by position
const failureAtPos1 = error.failuresByPos[1];
if (failureAtPos1) {
console.log('Second item failed:', failureAtPos1.message);
}
}
}Handle modify operations with partial success:
try {
const modifyCount = await db.users
.where('age').above(18)
.modify({ status: 'adult' });
console.log(`Modified ${modifyCount} users`);
} catch (error) {
if (error instanceof Dexie.ModifyError) {
console.log(`Successfully modified ${error.successCount} items`);
console.log(`Failed to modify ${error.failures.length} items`);
// Access failed keys
console.log('Failed keys:', error.failedKeys);
// Process individual failures
error.failures.forEach(failure => {
console.log('Individual failure:', failure.message);
});
}
}const db = new Dexie('MyDatabase');
db.version(1).stores({ users: '++id, name, email' });
try {
await db.open();
} catch (error) {
if (error instanceof Dexie.OpenFailedError) {
console.log('Failed to open database:', error.message);
if (error.inner) {
console.log('Underlying cause:', error.inner);
}
} else if (error instanceof Dexie.MissingAPIError) {
console.log('IndexedDB not supported in this environment');
} else if (error instanceof Dexie.VersionChangeError) {
console.log('Database version changed by another connection');
// Consider retrying after a delay
}
}// Check if database exists
try {
const exists = await Dexie.exists('MyDatabase');
console.log('Database exists:', exists);
} catch (error) {
if (error instanceof Dexie.MissingAPIError) {
console.log('Cannot check database existence - IndexedDB not available');
}
}
// Delete database with error handling
try {
await Dexie.delete('MyDatabase');
console.log('Database deleted successfully');
} catch (error) {
if (error instanceof Dexie.DatabaseClosedError) {
console.log('Database was already closed');
} else if (error instanceof Dexie.NoSuchDatabaseError) {
console.log('Database does not exist');
}
}async function withRetry<T>(
operation: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
// Don't retry on certain error types
if (error instanceof Dexie.InvalidArgumentError ||
error instanceof Dexie.ConstraintError) {
throw error;
}
if (attempt < maxRetries) {
// Exponential backoff
const delay = Math.pow(2, attempt) * 100;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
// Usage
try {
const result = await withRetry(() =>
db.users.add({ name: 'John', email: 'john@example.com' })
);
} catch (error) {
console.log('Operation failed after retries:', error.message);
}class DatabaseService {
private db: Dexie;
private fallbackStorage = new Map();
constructor() {
this.db = new Dexie('MyApp');
this.db.version(1).stores({ users: '++id, name, email' });
}
async addUser(user: { name: string; email: string }) {
try {
return await this.db.users.add(user);
} catch (error) {
if (error instanceof Dexie.MissingAPIError ||
error instanceof Dexie.DatabaseClosedError) {
// Fall back to in-memory storage
const id = Date.now();
this.fallbackStorage.set(id, { id, ...user });
console.warn('Using fallback storage due to DB error');
return id;
}
throw error;
}
}
async getUser(id: number) {
try {
return await this.db.users.get(id);
} catch (error) {
if (error instanceof Dexie.DatabaseClosedError) {
return this.fallbackStorage.get(id);
}
throw error;
}
}
}async function robustTransaction<T>(
db: Dexie,
tables: Table[],
operation: () => Promise<T>
): Promise<T> {
let retries = 3;
while (retries > 0) {
try {
return await db.transaction('rw', tables, operation);
} catch (error) {
if (error instanceof Dexie.TransactionInactiveError && retries > 1) {
retries--;
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 100));
continue;
} else if (error instanceof Dexie.AbortError) {
console.log('Transaction was aborted');
throw error;
} else {
throw error;
}
}
}
}function classifyError(error: Error): 'recoverable' | 'user-error' | 'fatal' {
if (error instanceof Dexie.TransactionInactiveError ||
error instanceof Dexie.AbortError ||
error instanceof Dexie.TimeoutError) {
return 'recoverable';
}
if (error instanceof Dexie.InvalidArgumentError ||
error instanceof Dexie.ConstraintError ||
error instanceof Dexie.DataError) {
return 'user-error';
}
return 'fatal';
}
async function handleDatabaseOperation<T>(operation: () => Promise<T>): Promise<T> {
try {
return await operation();
} catch (error) {
const errorType = classifyError(error);
switch (errorType) {
case 'recoverable':
// Log and potentially retry
console.warn('Recoverable database error:', error.message);
break;
case 'user-error':
// Show user-friendly message
console.error('Invalid operation:', error.message);
break;
case 'fatal':
// Report to error service
console.error('Fatal database error:', error);
break;
}
throw error;
}
}class DatabaseErrorLogger {
static logError(error: Error, context?: string) {
const errorInfo = {
name: error.name,
message: error.message,
context,
timestamp: new Date().toISOString(),
isDexieError: error instanceof Dexie.DexieError,
stack: error.stack
};
if (error instanceof Dexie.DexieError && error.inner) {
errorInfo['innerError'] = {
name: error.inner.name,
message: error.inner.message
};
}
console.error('Database Error:', errorInfo);
// Send to error reporting service
// errorReporter.report(errorInfo);
}
}
// Usage
try {
await db.users.add(userData);
} catch (error) {
DatabaseErrorLogger.logError(error, 'Adding new user');
throw error;
}Import and use error classes for type checking and throwing:
import Dexie, {
DexieError,
ModifyError,
BulkError
} from 'dexie';
// Access via Dexie static properties
const dbError = new Dexie.InvalidTableError('Table does not exist');
const constraintError = new Dexie.ConstraintError('Unique constraint violated');
// Direct constructor usage
const modifyError = new ModifyError(
'Modify operation failed',
[new Error('Individual failure')],
5, // successCount
['key1', 'key2'] // failedKeys
);
// Type checking
function isRecoverableError(error: Error): boolean {
return error instanceof Dexie.TransactionInactiveError ||
error instanceof Dexie.AbortError ||
error instanceof Dexie.TimeoutError;
}This comprehensive error handling system ensures robust database operations with clear error classification, appropriate recovery strategies, and detailed failure information for debugging and user feedback.
Install with Tessl CLI
npx tessl i tessl/npm-dexie