Advanced Topics

Error Handling & Safety

Error handling, WHERE validation, and safety features

The ORM includes several safety features to prevent common database mistakes and provides custom error classes for better error handling.

Custom Error Classes

The ORM provides specific error classes to help you handle different types of errors appropriately.

Error Hierarchy

import {
  TauriORMError,              // Base error class
  QueryBuilderError,          // Query building errors
  MissingWhereClauseError,    // Missing WHERE in UPDATE/DELETE
  ValidationError,            // General validation errors
  InsertValidationError,      // Insert-specific validation
  UpdateValidationError,      // Update-specific validation
  MigrationError,             // Migration errors
  RelationError,              // Relation errors
  ColumnNotFoundError,        // Column doesn't exist
  TableNotFoundError,         // Table doesn't exist
} from '@type32/tauri-sqlite-orm';

Catching Specific Errors

try {
  await db.update(users)
    .set({ name: 'Bob' })
    .execute(); // Missing WHERE clause
} catch (error) {
  if (error instanceof MissingWhereClauseError) {
    console.error('You forgot to add a WHERE clause!');
    console.error(error.message);
    // Handle this specific error type
  } else if (error instanceof UpdateValidationError) {
    console.error('Update validation failed');
  } else {
    // Re-throw unknown errors
    throw error;
  }
}

WHERE Clause Validation

Critical Safety Feature: UPDATE and DELETE operations require a WHERE clause by default to prevent accidental data loss.

The Problem

Without validation, it's easy to accidentally update or delete all rows:

// ❌ DANGEROUS: Would update ALL users if not prevented!
await db.update(users).set({ isActive: false }).execute();

// ❌ DANGEROUS: Would delete ALL posts if not prevented!
await db.delete(posts).execute();

The Solution

The ORM requires WHERE clauses for UPDATE and DELETE:

// ❌ Throws MissingWhereClauseError
await db.update(users)
  .set({ isActive: false })
  .execute();

// ✅ Works - has WHERE clause
await db.update(users)
  .set({ isActive: false })
  .where(eq(users._.columns.id, 123))
  .execute();

// ✅ Works - explicit global operation
await db.update(users)
  .set({ isActive: false })
  .allowGlobalOperation()  // ← Explicitly allows no WHERE clause
  .execute();

Error Message

MissingWhereClauseError: UPDATE operation on table "users" requires a WHERE clause to prevent accidental data loss. Use .where() to specify conditions, or use .allowGlobalOperation() to explicitly allow operations without WHERE.

Intentional Global Operations

When you genuinely need to update/delete all rows, use .allowGlobalOperation():

// Reset all daily counts - documented and intentional
await db.update(userStats)
  .set({ dailyCount: 0 })
  .allowGlobalOperation()
  .execute();

// Clear temporary cache
await db.delete(tempCache)
  .allowGlobalOperation()
  .execute();

Validation Errors

Insert Validation

try {
  // ❌ No data provided
  await db.insert(users).execute();
} catch (error) {
  if (error instanceof InsertValidationError) {
    console.error(error.message);
    // "No data provided for insert. Use .values() to provide data."
  }
}

Update Validation

try {
  // ❌ No SET clause
  await db.update(users)
    .where(eq(users._.columns.id, 1))
    .execute();
} catch (error) {
  if (error instanceof UpdateValidationError) {
    console.error(error.message);
    // "Cannot execute an update query without a .set(), .increment(), or .decrement() call."
  }
}

Column Not Found

try {
  await db.update(users)
    .increment('nonExistentColumn' as any)
    .where(eq(users._.columns.id, 1))
    .execute();
} catch (error) {
  if (error instanceof ColumnNotFoundError) {
    console.error(error.message);
    // "Column \"nonExistentColumn\" does not exist on table \"users\""
  }
}

Best Practices

1. Always Use WHERE Clauses

// ❌ BAD: Missing WHERE
await db.delete(users).execute();

// ✅ GOOD: Has WHERE
await db.delete(users)
  .where(eq(users._.columns.id, userId))
  .execute();

2. Catch Specific Errors

async function updateUser(userId: number, data: any) {
  try {
    return await db.update(users)
      .set(data)
      .where(eq(users._.columns.id, userId))
      .execute();
  } catch (error) {
    if (error instanceof MissingWhereClauseError) {
      // Log and handle missing WHERE
      logger.error('Missing WHERE clause in update');
      throw new Error('Invalid update operation');
    } else if (error instanceof ColumnNotFoundError) {
      // Handle invalid column
      logger.error('Invalid column in update:', error.message);
      throw new Error('Invalid field name');
    } else {
      // Re-throw database errors
      throw error;
    }
  }
}

3. Validate Before Operations

async function safeDeleteUser(userId: number) {
  // Check if user exists first
  const exists = await db.select(users)
    .where(eq(users._.columns.id, userId))
    .exists();
  
  if (!exists) {
    throw new ValidationError('User not found');
  }
  
  // Delete with proper WHERE clause
  return await db.delete(users)
    .where(eq(users._.columns.id, userId))
    .execute();
}

4. Document Global Operations

/**
 * Resets all user statistics for a new day
 * This intentionally updates ALL rows
 */
async function resetDailyStats() {
  await db.update(userStats)
    .set({ 
      dailyViews: 0,
      dailyLikes: 0 
    })
    .allowGlobalOperation()  // Documented intent
    .execute();
}

5. Use Type-Safe Operations

// TypeScript will catch column name errors at compile time
await db.update(users)
  .increment('views')  // ✓ Type-checked column name
  .where(eq(users._.columns.id, 1))
  .execute();

// ❌ TypeScript error if column doesn't exist
await db.update(users)
  .increment('invalidColumn')  // Type error!
  .execute();

Error Handling Pattern

Here's a comprehensive error handling pattern for production code:

import {
  MissingWhereClauseError,
  ColumnNotFoundError,
  ValidationError,
  TauriORMError
} from '@type32/tauri-sqlite-orm';

async function updateUserProfile(userId: number, updates: Partial<User>) {
  try {
    // Validate user exists
    const exists = await db.select(users)
      .where(eq(users._.columns.id, userId))
      .exists();
    
    if (!exists) {
      throw new ValidationError('User not found');
    }
    
    // Perform update
    const result = await db.update(users)
      .set({
        ...updates,
        updatedAt: new Date()
      })
      .where(eq(users._.columns.id, userId))
      .returningFirst();
    
    return result;
    
  } catch (error) {
    // Handle ORM-specific errors
    if (error instanceof TauriORMError) {
      logger.error('Database error:', {
        type: error.name,
        message: error.message,
        userId
      });
      
      // Provide user-friendly message
      if (error instanceof MissingWhereClauseError) {
        throw new Error('Invalid update operation');
      } else if (error instanceof ColumnNotFoundError) {
        throw new Error('Invalid field name');
      } else if (error instanceof ValidationError) {
        throw error; // Re-throw validation errors as-is
      }
    }
    
    // Handle other errors
    logger.error('Unexpected error:', error);
    throw new Error('Failed to update user');
  }
}

Summary

The ORM provides multiple layers of safety:

  1. Custom Error Classes - Clear, specific error messages
  2. WHERE Validation - Prevents accidental mass updates/deletes
  3. Type Safety - TypeScript catches errors at compile time
  4. Explicit Global Operations - .allowGlobalOperation() makes intent clear
  5. Validation Errors - Helpful messages for common mistakes

These features work together to make your database operations safer and more reliable in production applications.

Copyright © 2026