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.