{"record":{"id":"a7b85696ab8b5605","repo":"actualbudget/actual","slug":"transfer-category-with-id-transferid-not-found","errorCode":null,"errorMessage":"Transfer category with id ${transferId} not found.","messagePattern":"Transfer category with id (.+?) not found\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/budget/app.ts","lineNumber":392,"sourceCode":"}): Promise<void> {\n  await batchMessages(async () => {\n    const row = await db.first<Pick<db.DbCategory, 'is_income'>>(\n      'SELECT is_income FROM categories WHERE id = ?',\n      [id],\n    );\n    if (!row) {\n      throw new Error(`Category with id ${id} not found.`);\n    }\n\n    const transfer =\n      transferId &&\n      (await db.first<Pick<db.DbCategory, 'is_income'>>(\n        'SELECT is_income FROM categories WHERE id = ?',\n        [transferId],\n      ));\n\n    if (transferId && !transfer) {\n      throw new Error(`Transfer category with id ${transferId} not found.`);\n    } else if (\n      transferId &&\n      row &&\n      transfer &&\n      row.is_income !== transfer.is_income\n    ) {\n      throw new Error('Cannot transfer between income and expense categories.');\n    }\n\n    // Update spreadsheet values if it's an expense category\n    // TODO: We should do this for income too if it's a tracking budget\n    if (row.is_income === 0) {\n      if (transferId) {\n        await budget.doTransfer([id], transferId);\n      }\n    }\n\n    await db.deleteCategory({ id }, transferId);","sourceCodeStart":374,"sourceCodeEnd":410,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/budget/app.ts#L374-L410","documentation":"When deleting a category with a transferId, deleteCategory verifies the transfer target category exists before moving balances; a nonexistent transferId throws this error.","triggerScenarios":"deleteCategory(id, transferId) where transferId is stale, mistyped, or from another budget.","commonSituations":"Client UI passing a cached transfer-category selection after the target was deleted; passing a category-group id or payee id by mistake.","solutions":["Verify the transfer category exists before calling deleteCategory.","Refresh the transfer-category picker data (re-sync) so stale options disappear.","Pass null/undefined transferId if no transfer is intended (balances then go to 'Uncategorized')."],"exampleFix":"// before\nawait deleteCategory(catId, oldTransferId); // oldTransferId deleted earlier\n// after\nconst t = await q('categories').filter({ id: newTransferId }).select('*');\nif (t.length > 0) await deleteCategory(catId, newTransferId);","handlingStrategy":"validation","validationCode":"const transfer = await q('categories').filter({ id: transferId }).select('*').execute();\nif (transfer.length === 0) throw new Error(`Transfer category ${transferId} not found`);","typeGuard":null,"tryCatchPattern":"try {\n  await app.deleteCategory(id, transferId);\n} catch (e) {\n  if (e.message.includes('Transfer category')) {\n    await app.deleteCategory(id); // fall back: no transfer, balance to Uncategorized\n  } else throw e;\n}","preventionTips":["Re-query the transfer-category list when the delete dialog opens instead of using cached selections","Remove transfer options referencing deleted categories","Validate transfer ids right before submitting"],"tags":["budget","category","not-found","transfer"],"backgroundTag":"record-not-found","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}