{"record":{"id":"2ec2d6dcbc12eb6e","repo":"actualbudget/actual","slug":"report-recall-error","errorCode":null,"errorMessage":"Report recall error","messagePattern":"Report recall error","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/reports/app.ts","lineNumber":161,"sourceCode":"\n  const nameExists = await reportNameExists(item.name, item.id ?? '', true);\n  if (nameExists) {\n    throw new Error('There is already a report named ' + item.name);\n  }\n\n  // Create the report here based on the info\n  await db.insertWithSchema('custom_reports', reportModel.fromJS(item));\n\n  return reportId;\n}\n\nasync function updateReport(item: CustomReportEntity) {\n  if (!item.name) {\n    throw new Error('Report name is required');\n  }\n\n  if (!item.id) {\n    throw new Error('Report recall error');\n  }\n\n  const nameExists = await reportNameExists(item.name, item.id, false);\n  if (nameExists) {\n    throw new Error('There is already a report named ' + item.name);\n  }\n\n  await db.updateWithSchema('custom_reports', reportModel.fromJS(item));\n}\n\nasync function deleteReport(id: CustomReportEntity['id']) {\n  await db.delete_('custom_reports', id);\n}\n\nexport type ReportsHandlers = {\n  'report/get': typeof getReports;\n  'report/create': typeof createReport;\n  'report/update': typeof updateReport;","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/reports/app.ts#L143-L179","documentation":"updateReport requires a valid custom report id to update the existing row in the custom_reports table. When item.id is falsy (missing, null, or empty string), the function cannot recall which report to update, so it throws 'Report recall error' before any database work happens.","triggerScenarios":"Calling updateReport with a CustomReportEntity whose id field is undefined/null/empty — e.g. passing a newly constructed report object (as if creating) instead of a saved report, or deserializing a report where the id key was dropped.","commonSituations":"UI code building a report object from form state that never assigns the id; code reusing a createReport payload shape for updates; JSON round-trips that strip the id field.","solutions":["Ensure the item passed to updateReport was loaded from the database and retains its id","Generate/preserve an id before calling updateReport (e.g. via the report creation path) and pass it through","Distinguish create vs update flows: call createReport for new reports, updateReport only for existing ones"],"exampleFix":"// before\nawait updateReport({ name: 'Groceries', groupId: 'g1' }); // no id\n// after\nconst existing = await getReport(reportId);\nawait updateReport({ ...existing, name: 'Groceries' });","handlingStrategy":"validation","validationCode":"if (!item.id) throw new Error('Cannot update a report without an id'); await updateReport(item);","typeGuard":"function hasId(report: CustomReportEntity): report is CustomReportEntity & { id: string } {\n  return typeof report.id === 'string' && report.id.length > 0;\n}","tryCatchPattern":"try {\n  await updateReport(report);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Report recall error') {\n    // fall back to createReport or re-fetch the report\n  } else throw e;\n}","preventionTips":["Always load the report via a getter before updating instead of constructing payloads from scratch","Validate the id presence at the UI/form boundary before calling mutation APIs","Keep create vs update code paths clearly separated"],"tags":["validation","missing-identifier","reports"],"backgroundTag":"missing-identifier","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}