actualbudget/actual · error

Failed to delete user access

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.

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.

Appendix: 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)