actualbudget/actual · error · Error

throw Error(errorMsg)

Error message

throw Error(errorMsg)

What it means

During YNAB5 budget import, schedule creation retries with a suffixed name ('Name (1)', 'Name (2)') when a schedule with the same name exists. If the api/schedule-create call keeps failing after MAX_RETRY attempts, the last error is normalized and rethrown as a plain Error. This means the import could not create a schedule even after de-duplication attempts, so the failure is usually not about the name at all.

Source

Thrown at packages/loot-core/src/server/importers/ynab5.ts:890

    payee: string;
    account: string;
    amount: number;
    amountOp: 'is';
    date: RecurConfig | string;
  }) {
    const baseName = params.name;
    let count = 1;

    while (true) {
      try {
        return await send('api/schedule-create', {
          ...params,
          name: params.name,
        });
      } catch (e) {
        if (count >= MAX_RETRY) {
          const errorMsg = normalizeError(e);
          throw Error(errorMsg);
        }
        params.name = `${baseName} (${count})`;
        count += 1;
      }
    }
  }

  async function getRuleForSchedule(
    scheduleId: string,
  ): Promise<RuleEntity | null> {
    const { data: ruleId } = (await send('api/query', {
      query: q('schedules')
        .filter({ id: scheduleId })
        .calculate('rule')
        .serialize(),
    })) as { data: string | null };
    if (!ruleId) {
      return null;

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Inspect the normalized error message to find the real cause — it is the underlying schedule-create failure, not a name collision
  2. Validate the YNAB file's scheduled transaction date/recurrence data before import
  3. Check that the referenced payee and account were successfully imported and exist
  4. Retry the import after fixing the budget file; if it persists, file an issue with the schedule payload

Example fix

// before (retry loop rethrows opaque error)
if (count >= MAX_RETRY) {
  const errorMsg = normalizeError(e);
  throw Error(errorMsg);
}
// after (preserve cause and context)
if (count >= MAX_RETRY) {
  throw new Error(
    `Failed to create schedule "${baseName}" after ${count} attempts: ${normalizeError(e)}`,
    { cause: e },
  );
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check for an existing schedule with the same name before import
const existing = await send('api/query', {
  query: q('schedules').filter({ name: scheduleName }).select('id'),
});
if (existing.data.length > 0) console.warn(`Schedule "${scheduleName}" exists; importer will rename`);

Type guard

function isRetryableScheduleError(e: unknown): boolean {
  const msg = e instanceof Error ? e.message : String(e);
  return /already exists|duplicate/i.test(msg);
}

Try / catch

try {
  await doYnab5Import(buffer);
} catch (e) {
  const msg = e instanceof Error ? e.message : String(e);
  if (/schedule/i.test(msg)) {
    console.error('Schedule import failed:', msg); // inspect underlying cause
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: createScheduleWithUniqueName calls send('api/schedule-create', ...) and the call throws MAX_RETRY consecutive times: e.g. invalid recurring date config (bad RecurConfig from the YNAB file), invalid payee/account ids, or a persistent server-side validation error that does not disappear when the name changes.

Common situations: Importing a YNAB budget whose scheduled transaction has a malformed or unsupported recurrence pattern; corrupted ids referenced by the schedule; a bug/regression in schedule-create validation rejecting the payload regardless of name.

Related errors


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