actualbudget/actual · error

Failed to delete user access: ${error.message}

Error message

Failed to delete user access: ${error.message}

What it means

deleteUserAccess(userId) wraps any error from the DELETE on the user_access table and rethrows as 'Failed to delete user access: <original message>'. It preserves the underlying SQLite error (constraint, locked DB, etc.) while adding context about the operation.

Source

Thrown at packages/sync-server/src/services/user-service.ts:105

export function deleteUser(userId) {
  return getAccountDb().transaction(() => {
    const { changes } = getAccountDb().mutate(
      'DELETE FROM users WHERE id = ? and owner = 0',
      [userId],
    );
    if (changes > 0) {
      getAccountDb().mutate('DELETE FROM sessions WHERE user_id = ?', [userId]);
    }
    return changes;
  });
}
export function deleteUserAccess(userId) {
  try {
    return getAccountDb().mutate('DELETE FROM user_access WHERE user_id = ?', [
      userId,
    ]).changes;
  } catch (error) {
    throw new Error(`Failed to delete user access: ${error.message}`);
  }
}

export function transferAllFilesFromUser(ownerId, oldUserId) {
  if (!ownerId || !oldUserId) {
    throw new Error('Invalid user IDs');
  }
  try {
    getAccountDb().transaction(() => {
      const ownerExists = getUserById(ownerId);
      if (!ownerExists) {
        throw new Error('New owner not found');
      }
      getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [
        ownerId,
        oldUserId,
      ]);
    });

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Read the wrapped error.message for the root SQLite cause and fix that first.
  2. Ensure no other process holds a write lock on the account SQLite file; stop duplicate server instances.
  3. Run migrations so the user_access table exists in the account DB.
  4. Check filesystem permissions / disk space for the SQLite database location.

Example fix

// before
deleteUserAccess('user-123');   // Error: Failed to delete user access: SQLITE_BUSY: database is locked
// after
// stop the second server instance / retry after lock clears, then
deleteUserAccess('user-123');
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure the DB is writable and the table exists first
const row = getAccountDb().first('SELECT user_id FROM user_access WHERE user_id = ?', [userId]);
if (row === undefined) console.warn(`No user_access rows for ${userId}; nothing to delete`);

Type guard

function isUserAccessDeletionError(err: unknown): err is Error {
  return err instanceof Error && err.message.startsWith('Failed to delete user access:');
}

Try / catch

try {
  deleteUserAccess(userId);
} catch (err) {
  if (isUserAccessDeletionError(err)) {
    logger.error('user_access delete failed; underlying cause: %s', err.message.replace('Failed to delete user access: ', ''));
    // surface 500 and advise fixing DB locks/permissions, then retry
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: The DELETE FROM user_access WHERE user_id = ? fails — database file locked by another process, disk I/O error, corrupted schema, or missing user_access table (old/unmigrated account DB).

Common situations: Admin deleting a user while another sync-server process holds a write lock; running the server against a read-only filesystem; account DB from an older version lacking the user_access table.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/80b802963465d774. Report an issue: GitHub.