CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-dexie

A minimalistic wrapper for IndexedDB providing reactive queries, transactions, and schema management

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

error-handling.mddocs/

Error Handling

Comprehensive guide to Dexie's error system, error types, and handling strategies for robust database operations.

Overview

Dexie provides a sophisticated error handling system built around the DexieError base class and specialized error types for different database operations. The system includes:

  • Hierarchical error types extending from DexieError
  • IndexedDB error mapping to Dexie-specific errors
  • Enhanced promise catching with error type filtering
  • Bulk operation errors with detailed failure information
  • Transaction error handling with automatic rollback

Error Architecture

Base Error Class

The 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;
}

Error Types

Core Dexie Errors

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';
}

IndexedDB Mapped Errors

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';
}

Specialized Error Classes

ModifyError

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;
}

BulkError

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;
}

Error Handling Patterns

Basic Error Catching

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);
  }
}

Enhanced Promise Catching

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
  });

Transaction Error Handling

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);
  }
}

Bulk Operation Error Handling

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);
    }
  }
}

Collection Modify Error Handling

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);
    });
  }
}

Database Connection Errors

Opening and Version Errors

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

Database Existence and Deletion

// 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');
  }
}

Error Recovery Strategies

Retry with Backoff

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);
}

Graceful Degradation

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;
    }
  }
}

Transaction Recovery

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;
      }
    }
  }
}

Best Practices

Error Classification

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;
  }
}

Error Logging

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;
}

Accessing Error Classes

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

docs

database-management.md

error-handling.md

events.md

index.md

live-queries.md

query-building.md

schema-management.md

table-operations.md

utility-functions.md

tile.json