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
- Read the wrapped error.message for the root SQLite cause and fix that first.
- Ensure no other process holds a write lock on the account SQLite file; stop duplicate server instances.
- Run migrations so the user_access table exists in the account DB.
- 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
- Run only one sync-server process against the account SQLite DB.
- Confirm filesystem write permissions and free disk space for the DB.
- Keep migrations current so user_access exists.
- Back up the account DB before destructive admin operations.
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
- Failed to update file owner: ${error.message}
- Failed to add user access: ${error.message}
- Failed to retrieve owner count
- Could not update File
- File not found
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/80b802963465d774.
Report an issue: GitHub.