actualbudget/actual · error

No id returned from download.

Error message

No id returned from download.

What it means

`downloadBudget` calls `send('download-budget', { cloudFileId })`; when there is no `error`, the code assumes the server returned the new local budget `id`. If `id` is still falsy it throws 'No id returned from download.' — an invariant violation meaning the backend reported success but did not include the identifier needed to open the downloaded budget.

Source

Thrown at packages/desktop-client/src/budgetfiles/budgetfilesSlice.ts:370

                error.meta &&
                typeof error.meta === 'object' &&
                'name' in error.meta &&
                error.meta.name,
            },
          ),
        );

        return await dispatch(
          downloadBudget({ cloudFileId, replace: true }),
        ).unwrap();
      } else {
        dispatch(setAppState({ loadingText: null }));
        alert(getDownloadError(error));
      }
      return null;
    } else {
      if (!id) {
        throw new Error('No id returned from download.');
      }
      await Promise.all([
        dispatch(loadGlobalPrefs()),
        dispatch(loadAllFiles()),
        dispatch(loadBudget({ id })),
      ]);
      dispatch(setAppState({ loadingText: null }));
      return id;
    }
  },
);

type LoadBackupPayload = {
  budgetId: string;
  backupId: string;
};

// Take in the budget id so that backups can be loaded when a budget isn't opened

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Update both client and sync-server to matching versions so the `download-budget` response shape includes `id`.
  2. Check the sync-server logs for the download request; confirm the handler returns `{ id }` on success.
  3. Bypass proxies/interceptors that may alter the response body and retry the download.
  4. Recreate the cloud file (re-upload) if its server-side record is corrupt, then download again.

Example fix

// before
const { id, error } = await send('download-budget', { cloudFileId });
if (error) { ... }
// after
const { id, error } = await send('download-budget', { cloudFileId });
if (error) { ... }
if (!id) {
  throw new Error('Download succeeded but server returned no budget id. ' +
    'Verify client and sync-server versions match.');
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Pre-flight: confirm the cloud file exists and server is reachable
const { cloudFiles } = await send('get-budgets'); // or loadAllFiles state
const exists = cloudFiles.some(f => f.cloudFileId === cloudFileId);
if (!exists) { alert('Cloud file not found: ' + cloudFileId); return; }

Type guard

type DownloadResult = { id?: string; error?: unknown };
function hasDownloadId(
  res: DownloadResult,
): res is DownloadResult & { id: string } {
  return typeof res.id === 'string' && res.id.length > 0;
}

Try / catch

try {
  const id = await dispatch(downloadBudget({ cloudFileId })).unwrap();
  if (!id) { /* non-throwing error paths return null and alert internally */ return; }
} catch (e) {
  if (e instanceof Error && e.message === 'No id returned from download.') {
    alert('Server returned a malformed response. Check that client and sync-server versions match.');
  } else { throw e; }
}

Prevention

When it happens

Trigger: The `download-budget` IPC/server handler resolves without `error` but also without an `id` — typically a backend bug, a protocol/version mismatch between client and sync-server, or a custom/patched server that returns an empty success response for a cloud file download.

Common situations: Running a mismatched sync-server version against a newer client; proxy/middleware stripping the response payload; developing against a stubbed or partially implemented `download-budget` handler; response serialization dropping the id field.

Related errors


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