actualbudget/actual · error

Unsupported widget type: ${widget.type}

Error message

Unsupported widget type: ${widget.type}

What it means

Inside copyDashboardWidget, after loading the row, the code re-checks the widget type with isWidgetType before calling addDashboardWidget. Because the DB row's type is untyped, an unrecognized stored value reaches the else branch and throws `Unsupported widget type`. This is a defensive guard against corrupt or legacy rows.

Source

Thrown at packages/loot-core/src/server/dashboard/app.ts:256

  );

  if (!widget) {
    throw new Error(`Widget not found: ${id}`);
  }

  await batchMessages(async () => {
    // Insert the widget to target dashboard
    if (isWidgetType(widget.type)) {
      const newWidget = {
        type: widget.type,
        width: widget.width,
        height: widget.height,
        meta: widget.meta ? JSON.parse(widget.meta) : {},
        dashboard_page_id: targetDashboardPageId,
      };
      await addDashboardWidget(newWidget);
    } else {
      throw new Error(`Unsupported widget type: ${widget.type}`);
    }
  });
}

async function importDashboard({
  filePath,
  dashboardPageId,
}: {
  filePath: string;
  dashboardPageId: string;
}) {
  try {
    if (!(await fs.exists(filePath))) {
      throw new Error(`File not found at the provided path: ${filePath}`);
    }

    const content = await fs.readFile(filePath);
    const parsedContent: ExportImportDashboard = JSON.parse(content);

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Fix the widget row's `type` in the dashboard table to a supported value (see isWidgetType).
  2. Delete the invalid widget row (or tombstone it) and recreate the widget.
  3. Upgrade Actual to a version that supports the stored widget type.

Example fix

// before
UPDATE dashboard SET type='future-card' WHERE id='...';
// after
UPDATE dashboard SET type='net-worth-card' WHERE id='...';
Defensive patterns

Strategy: type-guard

Validate before calling

const SUPPORTED = ['net-worth-card','cash-flow-card','spending-card','crossover-card','budget-analysis-card','markdown-card','summary-card','calendar-card','formula-card','custom-report','sankey-card','balance-forecast-card','age-of-money-card','monte-carlo-card'];
const widget = await db.first('SELECT type FROM dashboard WHERE id = ?', [id]);
if (widget && !SUPPORTED.includes(widget.type)) {
  throw new Error(`Widget ${id} has unsupported stored type '${widget.type}'`);
}

Type guard

function isWidgetType(type: string): type is DashboardWidgetEntity['type'] {
  return ['net-worth-card','cash-flow-card','spending-card','crossover-card','budget-analysis-card','markdown-card','summary-card','calendar-card','formula-card','custom-report','sankey-card','balance-forecast-card','age-of-money-card','monte-carlo-card'].includes(type);
}

Try / catch

try {
  await send('dashboard-copy-widget', { id, targetDashboardPageId });
} catch (e) {
  if (e.message.startsWith('Unsupported widget type:')) {
    console.error('DB contains a widget type this build cannot render; upgrade Actual or fix the row.');
  }
}

Prevention

When it happens

Trigger: Copying a widget whose stored `type` column contains a value outside the supported list — e.g. data written by a newer version, manual DB edits, or corrupted rows.

Common situations: Downgraded Actual installs whose DB still holds widget types introduced later; rows tampered with via direct SQLite access; partially failed migrations.

Related errors


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