actualbudget/actual · error

Transfer category with id ${transferId} not found.

Error message

Transfer category with id ${transferId} not found.

What it means

When deleting a category with a transferId, deleteCategory verifies the transfer target category exists before moving balances; a nonexistent transferId throws this error.

Source

Thrown at packages/loot-core/src/server/budget/app.ts:392

}): Promise<void> {
  await batchMessages(async () => {
    const row = await db.first<Pick<db.DbCategory, 'is_income'>>(
      'SELECT is_income FROM categories WHERE id = ?',
      [id],
    );
    if (!row) {
      throw new Error(`Category with id ${id} not found.`);
    }

    const transfer =
      transferId &&
      (await db.first<Pick<db.DbCategory, 'is_income'>>(
        'SELECT is_income FROM categories WHERE id = ?',
        [transferId],
      ));

    if (transferId && !transfer) {
      throw new Error(`Transfer category with id ${transferId} not found.`);
    } else if (
      transferId &&
      row &&
      transfer &&
      row.is_income !== transfer.is_income
    ) {
      throw new Error('Cannot transfer between income and expense categories.');
    }

    // Update spreadsheet values if it's an expense category
    // TODO: We should do this for income too if it's a tracking budget
    if (row.is_income === 0) {
      if (transferId) {
        await budget.doTransfer([id], transferId);
      }
    }

    await db.deleteCategory({ id }, transferId);

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Verify the transfer category exists before calling deleteCategory.
  2. Refresh the transfer-category picker data (re-sync) so stale options disappear.
  3. Pass null/undefined transferId if no transfer is intended (balances then go to 'Uncategorized').

Example fix

// before
await deleteCategory(catId, oldTransferId); // oldTransferId deleted earlier
// after
const t = await q('categories').filter({ id: newTransferId }).select('*');
if (t.length > 0) await deleteCategory(catId, newTransferId);
Defensive patterns

Strategy: validation

Validate before calling

const transfer = await q('categories').filter({ id: transferId }).select('*').execute();
if (transfer.length === 0) throw new Error(`Transfer category ${transferId} not found`);

Try / catch

try {
  await app.deleteCategory(id, transferId);
} catch (e) {
  if (e.message.includes('Transfer category')) {
    await app.deleteCategory(id); // fall back: no transfer, balance to Uncategorized
  } else throw e;
}

Prevention

When it happens

Trigger: deleteCategory(id, transferId) where transferId is stale, mistyped, or from another budget.

Common situations: Client UI passing a cached transfer-category selection after the target was deleted; passing a category-group id or payee id by mistake.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/a7b85696ab8b5605. Report an issue: GitHub.