{"record":{"id":"cd61a00693b81b4a","repo":"actualbudget/actual","slug":"failed-to-add-user-access-error-message","errorCode":null,"errorMessage":"Failed to add user access: ${error.message}","messagePattern":"Failed to add user access: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/sync-server/src/services/user-service.ts","lineNumber":201,"sourceCode":"export function addUserAccess(userId, fileId) {\n  if (!userId || !fileId) {\n    throw new Error('Invalid parameters');\n  }\n  try {\n    const userExists = getUserById(userId);\n    const fileExists = getFileById(fileId);\n    if (!userExists || !fileExists) {\n      throw new Error('User or file not found');\n    }\n    getAccountDb().mutate(\n      'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)',\n      [userId, fileId],\n    );\n  } catch (error) {\n    if (error.message.includes('UNIQUE constraint')) {\n      throw new Error('Access already exists');\n    }\n    throw new Error(`Failed to add user access: ${error.message}`);\n  }\n}\n\nexport function deleteUserAccessByFileId(userIds, fileId) {\n  if (!Array.isArray(userIds) || userIds.length === 0) {\n    throw new Error('The provided userIds must be a non-empty array.');\n  }\n\n  const CHUNK_SIZE = 999;\n  let totalChanges = 0;\n\n  try {\n    getAccountDb().transaction(() => {\n      for (let i = 0; i < userIds.length; i += CHUNK_SIZE) {\n        const chunk = userIds.slice(i, i + CHUNK_SIZE);\n        const placeholders = chunk.map(() => '?').join(',');\n\n        const sql = `DELETE FROM user_access WHERE user_id IN (${placeholders}) AND file_id = ?`;","sourceCodeStart":183,"sourceCodeEnd":219,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/services/user-service.ts#L183-L219","documentation":"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.","triggerScenarios":"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.","commonSituations":"'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.","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"],"exampleFix":"// before\ncatch (e) { alert('Share failed'); }\n// after\ntry {\n  addUserAccess(userId, fileId);\n} catch (e) {\n  if (e.message.includes('database is locked')) {\n    await retry(() => addUserAccess(userId, fileId));\n  } else {\n    alert(`Share failed: ${e.message}`);\n  }\n}","handlingStrategy":"try-catch","validationCode":"if (!userId || !fileId) throw new Error('userId and fileId required');\nif (!getUserById(userId) || !getFileById(fileId)) {\n  throw new Error('user or file does not exist');\n}","typeGuard":"function canAttemptGrant(userId, fileId) {\n  return Boolean(userId) && Boolean(fileId) &&\n         getUserById(userId) !== null && getFileById(fileId) !== null;\n}","tryCatchPattern":"try {\n  addUserAccess(userId, fileId);\n} catch (e) {\n  const cause = e.message.replace('Failed to add user access: ', '');\n  if (cause.includes('database is locked')) {\n    await backoffRetry(() => addUserAccess(userId, fileId), 3);\n  } else {\n    logger.error({ cause }, 'addUserAccess failed');\n    throw e;\n  }\n}","preventionTips":["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"],"tags":["sync-server","error-wrapping","sqlite","database"],"backgroundTag":"wrapped-error-chain","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}