actualbudget/actual · error
Failed to add user access: ${error.message}
Error message
Failed to add user access: ${error.message} What it means
addUserAccess catches all errors from its try block and, if not a UNIQUE constraint violation, rethrows them as `Failed to add user access: <cause>`. This covers failures from the existence checks and the INSERT itself (locked db, disk error, schema problems). The original message is appended after the colon.
Source
Thrown at packages/sync-server/src/services/user-service.ts:201
export function addUserAccess(userId, fileId) {
if (!userId || !fileId) {
throw new Error('Invalid parameters');
}
try {
const userExists = getUserById(userId);
const fileExists = getFileById(fileId);
if (!userExists || !fileExists) {
throw new Error('User or file not found');
}
getAccountDb().mutate(
'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)',
[userId, fileId],
);
} catch (error) {
if (error.message.includes('UNIQUE constraint')) {
throw new Error('Access already exists');
}
throw new Error(`Failed to add user access: ${error.message}`);
}
}
export function deleteUserAccessByFileId(userIds, fileId) {
if (!Array.isArray(userIds) || userIds.length === 0) {
throw new Error('The provided userIds must be a non-empty array.');
}
const CHUNK_SIZE = 999;
let totalChanges = 0;
try {
getAccountDb().transaction(() => {
for (let i = 0; i < userIds.length; i += CHUNK_SIZE) {
const chunk = userIds.slice(i, i + CHUNK_SIZE);
const placeholders = chunk.map(() => '?').join(',');
const sql = `DELETE FROM user_access WHERE user_id IN (${placeholders}) AND file_id = ?`;View on GitHub (pinned to d4334cb6e6)
Solutions
- Read the cause after the colon to identify the actual failure
- For 'database is locked', reduce concurrent writes or retry with backoff
- Check account.sqlite file permissions and available disk space on the server
- If the cause is 'User or file not found' or 'Access already exists', handle those specific cases per their own guidance
Example fix
// before
catch (e) { alert('Share failed'); }
// after
try {
addUserAccess(userId, fileId);
} catch (e) {
if (e.message.includes('database is locked')) {
await retry(() => addUserAccess(userId, fileId));
} else {
alert(`Share failed: ${e.message}`);
}
} Defensive patterns
Strategy: try-catch
Validate before calling
if (!userId || !fileId) throw new Error('userId and fileId required');
if (!getUserById(userId) || !getFileById(fileId)) {
throw new Error('user or file does not exist');
} Type guard
function canAttemptGrant(userId, fileId) {
return Boolean(userId) && Boolean(fileId) &&
getUserById(userId) !== null && getFileById(fileId) !== null;
} Try / catch
try {
addUserAccess(userId, fileId);
} catch (e) {
const cause = e.message.replace('Failed to add user access: ', '');
if (cause.includes('database is locked')) {
await backoffRetry(() => addUserAccess(userId, fileId), 3);
} else {
logger.error({ cause }, 'addUserAccess failed');
throw e;
}
} Prevention
- Log the full wrapped message to expose the root cause
- Retry only transient SQLite errors; surface 'User or file not found' as 404
- Monitor server disk space and account.sqlite permissions
- Pre-validate user/file existence to skip the wrapper path
When it happens
Trigger: Any non-duplicate failure inside addUserAccess: a SQLite error during INSERT (database locked, read-only file, disk full), or an unexpected error thrown by getUserById/getFileById lookups.
Common situations: 'database is locked' under concurrent sync-server writes; permission problems after moving the data directory; disk exhaustion on self-hosted servers; debugging share failures where the real cause is hidden by the wrapper text.
Related errors
- Failed to update file owner: ${error.message}
- Failed to delete user access: ${error.message}
- Failed to transfer files: ${error.message}
- Failed to retrieve owner count
- Could not update File
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/cd61a00693b81b4a.
Report an issue: GitHub.