actualbudget/actual · error

Category with id ${id} not found.

Error message

Category with id ${id} not found.

What it means

deleteCategory first fetches the category row to determine whether it is an income category; if no row exists for the given id, it throws instead of silently deleting nothing.

Source

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

  await batchMessages(async () => {
    await db.moveCategory(id, groupId, targetId);
  });
}

async function deleteCategory({
  id,
  transferId,
}: {
  id: CategoryEntity['id'];
  transferId?: CategoryEntity['id'] | null;
}): 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.');

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Verify the category id exists (q('categories').filter({ id }).select('*')) before deleting.
  2. Refresh local state/sync and retry so stale ids are replaced.
  3. Guard the UI delete action against double-submission.

Example fix

// before
await deleteCategory(catId); // catId may be stale
// after
const rows = await q('categories').filter({ id: catId }).select('*');
if (rows.length > 0) await deleteCategory(catId);
Defensive patterns

Strategy: validation

Validate before calling

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

Type guard

function categoryExists(row: { id: string } | null | undefined): boolean {
  return row != null;
}

Try / catch

try {
  await app.deleteCategory(id);
} catch (e) {
  if (e.message.includes('not found')) {
    refreshCategories(); // resync to clear stale ids
  } else throw e;
}

Prevention

When it happens

Trigger: Calling app.deleteCategory(id) with an id that is not in the categories table (already deleted, wrong id, or id from a different budget file).

Common situations: Stale client state after a sync; deleting the same category twice from racing UI actions; using a group id instead of a category id.

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/c0e7ce33a74c70f3. Report an issue: GitHub.