{"record":{"id":"638828119a574912","repo":"actualbudget/actual","slug":"file-not-found","errorCode":null,"errorMessage":"File not found","messagePattern":"File not found","errorType":"exception","errorClass":"GenericFileError","httpStatus":null,"severity":"error","filePath":"packages/sync-server/src/app-sync/services/files-service.ts","lineNumber":263,"sourceCode":"      updates.push('deleted = ?');\n      params.push(boolToInt(fileUpdate.deleted));\n    }\n\n    if (updates.length > 0) {\n      query += ' ' + updates.join(', ') + ' WHERE id = ?';\n      params.push(id);\n\n      const res = this.accountDb.mutate(query, params);\n\n      if (res.changes !== 1) {\n        throw new GenericFileError('Could not update File', { id });\n      }\n    }\n\n    // Return the modified object\n    const rawFile = this.getRaw(id);\n    if (!rawFile) {\n      throw new GenericFileError('File not found', { id });\n    }\n    return this.validate(rawFile);\n  }\n\n  getRaw(fileId: FileId): RawFile | null {\n    return this.accountDb.first(`SELECT * FROM files WHERE id = ?`, [fileId]);\n  }\n\n  validate(rawFile: RawFile) {\n    const fileId = rawFile.id;\n    if (!isValidFileId(fileId)) {\n      throw new GenericFileError('Invalid file ID', { fileId });\n    }\n\n    let groupId: GroupId | null = null;\n    if (rawFile.group_id !== null) {\n      if (!isValidGroupId(rawFile.group_id)) {\n        throw new GenericFileError('Invalid group ID', {","sourceCodeStart":245,"sourceCodeEnd":281,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-sync/services/files-service.ts#L245-L281","documentation":"GenericFileError thrown at the end of FilesService.update(): after the UPDATE ran (possibly affecting rows loosely), the code re-fetches the row with getRaw(id) and throws if it is null. This guards the return path — the file must exist and be readable after an update.","triggerScenarios":"The row was deleted after the UPDATE statement executed; getRaw returns null because the id never existed and the update's WHERE clause silently matched nothing before this check; database read inconsistency mid-transaction.","commonSituations":"Concurrent DELETE racing an in-flight update; calling update with a bogus id against a build where changes is not checked; hard-resetting the account DB while requests are open.","solutions":["Verify the file ID exists before calling update().","Handle the concurrent-delete case: re-list files and stop updating deleted ones.","Serialize file mutations (single writer / transactions) to avoid read-after-write races.","Catch GenericFileError in the route and respond 404 instead of 500."],"exampleFix":"// before\nconst f = filesService.update(id, patch);   // throws 'File not found'\n// after\nconst f = filesService.getRaw(id) && filesService.update(id, patch);","handlingStrategy":"try-catch","validationCode":"const raw = filesService.getRaw(id);\nif (!raw) throw new Error(`File ${id} does not exist; skipping update`);","typeGuard":"function isFileMissingAfterUpdate(err: unknown): err is Error {\n  return err instanceof Error && err.message === 'File not found';\n}","tryCatchPattern":"try {\n  return filesService.update(id, patch);\n} catch (err) {\n  if (isFileMissingAfterUpdate(err)) {\n    // row vanished mid-update: re-list files and surface 404 to the client\n    return null;\n  }\n  throw err;\n}","preventionTips":["Never call update with IDs not obtained from a fresh get/list.","Avoid deleting files while sync requests are in flight.","Use transactions so read-modify-write sequences are atomic.","Map this error to a 404 response in route handlers."],"tags":["file-not-found","database","sync-server"],"backgroundTag":"resource-not-found","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}