{"record":{"id":"bbbd95057d7df02f","repo":"actualbudget/actual","slug":"debug-category-id-does-not-exist","errorCode":null,"errorMessage":"${debug}: category \"${id}\" does not exist","messagePattern":"(.+?): category \"(.+?)\" does not exist","errorType":"validation","errorClass":"APIError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/api.ts","lineNumber":115,"sourceCode":"    const range = monthUtils.range(start, end);\n    if (!range.includes(month)) {\n      throw APIError('No budget exists for month: ' + month);\n    }\n  }\n}\n\nasync function validateExpenseCategory(debug, id) {\n  if (id == null) {\n    throw APIError(`${debug}: category id is required`);\n  }\n\n  const row = await db.first<Pick<db.DbCategory, 'is_income'>>(\n    'SELECT is_income FROM categories WHERE id = ?',\n    [id],\n  );\n\n  if (!row) {\n    throw APIError(`${debug}: category \"${id}\" does not exist`);\n  }\n\n  if (row.is_income !== 0) {\n    throw APIError(`${debug}: category \"${id}\" is not an expense category`);\n  }\n}\n\nfunction checkFileOpen() {\n  if (!(prefs.getPrefs() || {}).id) {\n    throw APIError('No budget file is open');\n  }\n}\n\nlet batchPromise = null;\n\nhandlers['api/batch-budget-start'] = async function () {\n  if (batchPromise) {\n    throw APIError('Cannot start a batch process: batch already started');","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/api.ts#L97-L133","documentation":"validateExpenseCategory throws APIError when the supplied category id does not match any row in the categories table. The id is syntactically present but refers to a non-existent category in the currently open budget. The debug prefix identifies the failing API method.","triggerScenarios":"Calling an API method with a category id that was deleted, belongs to another budget file, was never created, or is a stale id cached from a previous export/sync.","commonSituations":"Hard-coded category ids copied from another budget; ids left in scripts after the category was renamed/deleted (deletes create tombstones, so old ids fail); switching budget files between API calls; mistyped id strings.","solutions":["Fetch current ids with q.getCategories() and use the matching category's id by name","Create the missing category first via q.createCategory(...) and use the returned id","Remove stale cached ids and re-resolve them at runtime instead of hard-coding","Confirm the same budget file is open when the id was captured and when it is used"],"exampleFix":"// before\nawait q.createTransaction({ accountId, date, amount, category: 'cat-groceries-old-id' });\n// APIError: ...category \"cat-groceries-old-id\" does not exist\n// after\nconst categories = await q.getCategories();\nconst groceries = categories.find(c => c.name === 'Groceries');\nawait q.createTransaction({ accountId, date, amount, category: groceries.id });","handlingStrategy":"validation","validationCode":"const categories = await q.getCategories();\nconst valid = categories.some(c => c.id === categoryId);\nif (!valid) throw new Error(`category ${categoryId} not found in this budget`);","typeGuard":null,"tryCatchPattern":"try {\n  await q.createTransaction(tx);\n} catch (e) {\n  if (/does not exist/.test(e.message)) {\n    const fresh = await q.getCategories();\n    tx.category = fresh.find(c => c.name === 'Groceries').id;\n    await q.createTransaction(tx);\n  } else throw e;\n}","preventionTips":["Resolve category ids by name via getCategories() at runtime instead of hard-coding","Re-resolve ids after any category delete/rename","Confirm the same budget file is open across API calls","Cache ids only per-budget and invalidate on budget switch"],"tags":["validation","api","category","not-found"],"backgroundTag":"entity-not-found","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}