actualbudget/actual · error · Error

Error importing budget: no budget was loaded

Error message

Error importing budget: no budget was loaded

What it means

After the 'import-budget' command reports success, the API checks that a budget id came back. If result.id is missing, something accepted the file but no budget was actually loaded, so the API throws rather than returning an unusable id. This is a defensive consistency check in packages/api/methods.ts.

Source

Thrown at packages/api/methods.ts:81

  {
    type = 'actual',
    filename,
  }: { type?: ImportableBudgetType; filename?: string } = {},
): Promise<{ id: string }> {
  const result =
    typeof input === 'string'
      ? await send('import-budget', { filepath: input, type })
      : await send('import-budget', {
          buffer: toArrayBuffer(input),
          filename,
          type,
        });

  if (result.error) {
    throw new Error(`Error importing budget: ${result.error}`);
  }
  if (!result.id) {
    throw new Error('Error importing budget: no budget was loaded');
  }
  return { id: result.id };
}

/** Export the currently-loaded budget as a zip buffer. */
export async function exportBudget(): Promise<Uint8Array> {
  const result = await send('export-budget');

  if ('error' in result) {
    throw new Error(`Error exporting budget: ${result.error}`);
  }
  if (!result.data) {
    throw new Error('Error exporting budget: no data was returned');
  }
  return new Uint8Array(result.data);
}

function toArrayBuffer(data: ArrayBuffer | Uint8Array): ArrayBuffer {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Align @actual-app/api and the sync-server/core to the same version (yarn install / rebuild).
  2. Log the raw response of the import by calling send('import-budget') directly (loot-core) to see what id was returned.
  3. Retry the import; verify the file opens correctly in the Actual UI.
  4. Report upstream with the file and versions if it reproduces.

Example fix

// before
const { id } = await api.importBudget(buf, 'budget.zip', 'application/zip');
// after
let result;
try {
  result = await api.importBudget(buf, 'budget.zip', 'application/zip');
} catch (e) {
  if (String(e.message).includes('no budget was loaded')) {
    // check api/core version parity before retrying
    throw new Error('Import returned no budget id — check @actual-app/api and server versions');
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

null

Type guard

function hasBudgetId(r: unknown): r is { id: string } {
  return typeof r === 'object' && r !== null && typeof (r as { id?: unknown }).id === 'string' && (r as { id: string }).id.length > 0;
}

Try / catch

try {
  const { id } = await api.importBudget(buf, 'budget.zip', 'application/zip');
} catch (e) {
  if (String(e.message).includes('no budget was loaded')) {
    // version/contract mismatch — check api vs server versions and retry
    throw new Error('Import succeeded but no budget id returned; verify package versions');
  }
  throw e;
}

Prevention

When it happens

Trigger: The core import handler resolves without an explicit error but fails to set the returned budget id — typically a silent success path in the import handler or an unexpected response shape from send('import-budget').

Common situations: Version mismatch between @actual-app/api and the core/server where the response contract changed; importing into a server session that did not fully initialize.

Related errors


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