{"record":{"id":"0366d38122c8f87c","repo":"actualbudget/actual","slug":"failed-to-update-file-owner-error-message","errorCode":null,"errorMessage":"Failed to update file owner: ${error.message}","messagePattern":"Failed to update file owner: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/sync-server/src/services/user-service.ts","lineNumber":142,"sourceCode":"  } catch (error) {\n    throw new Error(`Failed to transfer files: ${error.message}`);\n  }\n}\n\nexport function updateFileOwner(ownerId, fileId) {\n  if (!ownerId || !fileId) {\n    throw new Error('Invalid parameters');\n  }\n  try {\n    const result = getAccountDb().mutate(\n      'UPDATE files set owner = ? WHERE id = ?',\n      [ownerId, fileId],\n    );\n    if (result.changes === 0) {\n      throw new Error('File not found');\n    }\n  } catch (error) {\n    throw new Error(`Failed to update file owner: ${error.message}`);\n  }\n}\n\nexport function getUserAccess(fileId, userId, isAdmin) {\n  return getAccountDb().all(\n    `SELECT users.id as userId, user_name as userName, files.owner, display_name as displayName\n     FROM users\n     JOIN user_access ON user_access.user_id = users.id\n     JOIN files ON files.id = user_access.file_id\n     WHERE files.id = ? and (files.owner = ? OR 1 = ?)`,\n    [fileId, userId, isAdmin ? 1 : 0],\n  );\n}\n\nexport function countUserAccess(fileId, userId) {\n  const { accessCount } =\n    getAccountDb().first(\n      `SELECT COUNT(*) as accessCount","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/services/user-service.ts#L124-L160","documentation":"updateFileOwner wraps every error from its try block — including its own 'Invalid parameters' and 'File not found' throws, and any SQLite failure — in `Failed to update file owner: <cause>`. It signals the ownership change did not happen; the original reason is appended to the message.","triggerScenarios":"Any failure within updateFileOwner: falsy arguments, zero-row UPDATE ('File not found'), or a database error from the mutate call (locked database, read-only file, disk I/O error).","commonSituations":"Logs showing only the wrapper and obscuring the real cause; SQLite 'database is locked' under concurrent sync-server writes; permission problems after moving the data directory; debugging why an ownership change silently failed in an admin UI.","solutions":["Inspect the suffix of the message to find the root cause before changing code","For 'File not found', verify the fileId exists (see error 393 solutions)","For SQLite errors, check file permissions, free disk space, and concurrent writer contention on account.sqlite","Catch the error at the call site and surface the full message to admins rather than a generic failure"],"exampleFix":"// before\ntry {\n  updateFileOwner(ownerId, fileId);\n} catch (e) {\n  flash('Could not update owner');\n}\n// after\ntry {\n  updateFileOwner(ownerId, fileId);\n} catch (e) {\n  if (e.message.includes('database is locked')) {\n    await retryWithBackoff(() => updateFileOwner(ownerId, fileId));\n  } else {\n    flash(`Could not update owner: ${e.message}`);\n  }\n}","handlingStrategy":"try-catch","validationCode":"if (!ownerId || !fileId) throw new Error('ownerId and fileId required');\nif (!getFileById(fileId)) throw new Error(`File ${fileId} not found`);","typeGuard":"function canUpdateOwner(ownerId, fileId) {\n  return Boolean(ownerId) && Boolean(fileId) && getFileById(fileId) !== null;\n}","tryCatchPattern":"try {\n  updateFileOwner(ownerId, fileId);\n} catch (e) {\n  const cause = e.message.replace('Failed to update file owner: ', '');\n  if (cause.includes('database is locked')) {\n    await backoffRetry(() => updateFileOwner(ownerId, fileId), 3);\n  } else {\n    logger.error({ cause }, 'updateFileOwner failed');\n    throw e;\n  }\n}","preventionTips":["Parse the cause from the wrapped message before choosing a recovery path","Pre-validate ids and file existence to avoid the wrapper entirely","Retry only transient SQLite errors (locked), never 'File not found'","Monitor disk space and file permissions on the server data directory"],"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"}