actualbudget/actual · error · APIError

${debug}: category "${id}" does not exist

Error message

${debug}: category "${id}" does not exist

What it means

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.

Source

Thrown at packages/loot-core/src/server/api.ts:115

    const range = monthUtils.range(start, end);
    if (!range.includes(month)) {
      throw APIError('No budget exists for month: ' + month);
    }
  }
}

async function validateExpenseCategory(debug, id) {
  if (id == null) {
    throw APIError(`${debug}: category id is required`);
  }

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

  if (!row) {
    throw APIError(`${debug}: category "${id}" does not exist`);
  }

  if (row.is_income !== 0) {
    throw APIError(`${debug}: category "${id}" is not an expense category`);
  }
}

function checkFileOpen() {
  if (!(prefs.getPrefs() || {}).id) {
    throw APIError('No budget file is open');
  }
}

let batchPromise = null;

handlers['api/batch-budget-start'] = async function () {
  if (batchPromise) {
    throw APIError('Cannot start a batch process: batch already started');

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Fetch current ids with q.getCategories() and use the matching category's id by name
  2. Create the missing category first via q.createCategory(...) and use the returned id
  3. Remove stale cached ids and re-resolve them at runtime instead of hard-coding
  4. Confirm the same budget file is open when the id was captured and when it is used

Example fix

// before
await q.createTransaction({ accountId, date, amount, category: 'cat-groceries-old-id' });
// APIError: ...category "cat-groceries-old-id" does not exist
// after
const categories = await q.getCategories();
const groceries = categories.find(c => c.name === 'Groceries');
await q.createTransaction({ accountId, date, amount, category: groceries.id });
Defensive patterns

Strategy: validation

Validate before calling

const categories = await q.getCategories();
const valid = categories.some(c => c.id === categoryId);
if (!valid) throw new Error(`category ${categoryId} not found in this budget`);

Try / catch

try {
  await q.createTransaction(tx);
} catch (e) {
  if (/does not exist/.test(e.message)) {
    const fresh = await q.getCategories();
    tx.category = fresh.find(c => c.name === 'Groceries').id;
    await q.createTransaction(tx);
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


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