{"record":{"id":"194c7deb16f6d9bf","repo":"actualbudget/actual","slug":"failed-to-transfer-files-error-message","errorCode":null,"errorMessage":"Failed to transfer files: ${error.message}","messagePattern":"Failed to transfer files: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/sync-server/src/services/user-service.ts","lineNumber":125,"sourceCode":"}\n\nexport function transferAllFilesFromUser(ownerId, oldUserId) {\n  if (!ownerId || !oldUserId) {\n    throw new Error('Invalid user IDs');\n  }\n  try {\n    getAccountDb().transaction(() => {\n      const ownerExists = getUserById(ownerId);\n      if (!ownerExists) {\n        throw new Error('New owner not found');\n      }\n      getAccountDb().mutate('UPDATE files set owner = ? WHERE owner = ?', [\n        ownerId,\n        oldUserId,\n      ]);\n    });\n  } 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  }","sourceCodeStart":107,"sourceCodeEnd":143,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/services/user-service.ts#L107-L143","documentation":"transferAllFilesFromUser catches any error raised inside its try block — including the 'Invalid user IDs' and 'New owner not found' errors it throws itself — and rethrows it wrapped as `Failed to transfer files: <original message>`. This wrapper indicates the ownership-transfer transaction did not complete. The original cause is preserved in the message suffix.","triggerScenarios":"Any failure inside the function: falsy ownerId/oldUserId ('Invalid user IDs'), missing owner ('New owner not found'), or an underlying SQLite error during the UPDATE files statement (locked db, disk error, schema mismatch).","commonSituations":"Seeing only the generic prefix in logs and missing the real cause; OpenID finalize handlers hitting 'New owner not found' wrapped here; a locked or read-only account database producing SQLite errors wrapped here.","solutions":["Read the full message after the colon to identify the underlying cause ('Invalid user IDs', 'New owner not found', or a SQLite error)","If the cause is 'New owner not found', provision/verify the owner user before the transfer call","If the cause is a SQLite error, check database file permissions, disk space, and that no other process holds a write lock","Call transferAllFilesFromUser inside your own try/catch and log error.message fully rather than truncating it"],"exampleFix":"// before\nawait transferAllFilesFromUser(ownerId, oldUserId); // opaque wrapped error\n// after\ntry {\n  await transferAllFilesFromUser(ownerId, oldUserId);\n} catch (e) {\n  logger.error('transfer failed, cause:', e.message); // shows 'Failed to transfer files: New owner not found'\n  if (e.message.includes('New owner not found')) {\n    await provisionUser(ownerId);\n    await transferAllFilesFromUser(ownerId, oldUserId);\n  }\n}","handlingStrategy":"try-catch","validationCode":"function canTransfer(ownerId, oldUserId) {\n  return Boolean(ownerId) && Boolean(oldUserId) && getUserById(ownerId) !== null;\n}\nif (!canTransfer(ownerId, oldUserId)) throw new Error('precheck failed');","typeGuard":"function isTransferable(ownerId, oldUserId) {\n  return typeof ownerId === 'string' && ownerId.length > 0 &&\n         typeof oldUserId === 'string' && oldUserId.length > 0;\n}","tryCatchPattern":"try {\n  transferAllFilesFromUser(ownerId, oldUserId);\n} catch (e) {\n  const cause = e.message.replace('Failed to transfer files: ', '');\n  logger.error({ cause }, 'file transfer failed');\n  if (cause.includes('New owner not found')) await provisionAndRetry(ownerId, oldUserId);\n  else throw e;\n}","preventionTips":["Log the full error.message — the root cause is after the wrapper prefix","Validate both ids and owner existence before calling","Run transfers during low-write windows to avoid SQLite lock contention","Wrap retries around the specific cause, not the wrapper text"],"tags":["sync-server","error-wrapping","database","openid"],"backgroundTag":"wrapped-error-chain","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}