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
- Align @actual-app/api and the sync-server/core to the same version (yarn install / rebuild).
- Log the raw response of the import by calling send('import-budget') directly (loot-core) to see what id was returned.
- Retry the import; verify the file opens correctly in the Actual UI.
- 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
- Keep @actual-app/api, loot-core, and sync-server versions in lockstep.
- After upgrades, restart the API process before importing.
- Test imports against a scratch budget file after every dependency bump.
- Report persistent no-id imports upstream with versions.
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
- Error importing budget: ${result.error}
- Error exporting budget: no data was returned
- Error exporting budget: ${result.error}
- zipMeta ? getUnsafeZipError(zipMeta) : error
- Amount to hold needs to be greater than 0
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/d430c6ba18e4bb86.
Report an issue: GitHub.