actualbudget/actual · error

Widget not found: ${id}

Error message

Widget not found: ${id}

What it means

copyDashboardWidget looks up the source widget by id in the dashboard table (tombstone = 0). If no live row matches the supplied id, it throws `Widget not found: ${id}` before performing the copy.

Source

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

async function removeDashboardWidget(widgetId: string) {
  await db.delete_('dashboard', widgetId);
}

async function copyDashboardWidget({
  id,
  targetDashboardPageId,
}: {
  id: string;
  targetDashboardPageId: string;
}) {
  // Get the widget to copy
  const widget = await db.first<db.DbDashboard>(
    'SELECT * FROM dashboard WHERE id = ? AND tombstone = 0',
    [id],
  );

  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}`);
    }
  });
}

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Verify the widget id exists: run an AQL/SQL query on the dashboard table filtering tombstone = 0.
  2. Re-fetch the current widget list (e.g. via get-dashboard or a query) and use a fresh id.
  3. If the widget was tombstoned, re-create it rather than copying it.

Example fix

// before
await send('dashboard-copy-widget', { id: staleId });
// after
const widgets = await send('dashboard-get-widgets');
if (widgets.some(w => w.id === id)) {
  await send('dashboard-copy-widget', { id });
}
Defensive patterns

Strategy: validation

Validate before calling

const widgets = await send('dashboard-get-widgets');
if (!widgets.some(w => w.id === id)) {
  throw new Error(`Widget ${id} does not exist; refetch before copying.`);
}

Type guard

function widgetExists(widgets: { id: string }[], id: string): boolean {
  return widgets.some(w => w.id === id);
}

Try / catch

try {
  await send('dashboard-copy-widget', { id, targetDashboardPageId });
} catch (e) {
  if (e.message.startsWith('Widget not found:')) {
    console.error(`Widget ${id} is gone; refresh the widget list.`);
  }
}

Prevention

When it happens

Trigger: Calling the copy-dashboard-widget handler with an id that was deleted (tombstoned), a fabricated id, or an id from a different budget file.

Common situations: Scripts or plugins caching widget ids across budget resets; UI race where the widget was deleted before the copy request; copying ids between two budget files.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


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