Skip to content
KONIGI Patterns

Dashboards / Dashboard features / Error and stale state

‹ 6 of 10 ›

Error and stale state

People need to distinguish valid results from missing, failed, or outdated data.

Updated September 10, 2026

Problem

A failed or outdated query can look like a healthy value if the panel shows zero or silently keeps the last result.

Solution

Distinguish failed, missing, partial, and outdated data from a valid result. Never substitute zero for a failed query.

If the last known value is useful, keep it with a visible timestamp and stale label. Show gaps instead of implying continuous measurements where data is absent.

For partial results, say which sources or series are missing. Explain the next available action, such as retrying or checking the source. Keep the warning visible without requiring hover.

Use when

A panel loads data that can fail, arrive late, or be incomplete.

Don’t use when

There is no case for skipping it. The only real decision is loudness, and that scales with consequence: a degraded panel on a wallboard driving on-call decisions needs to be unmissable, while one on an exploratory page can be quieter.

Trade-offs

Warnings take space, and frequent failures can make people ignore them. Keeping old values supports continuity but risks mistaken decisions if their age is unclear. Partial results need enough detail to show what can still be trusted.

Checklist

  • Can a failed query ever render as a number, including as zero?
  • Is stale visually distinct from current, and does the panel say how old?
  • What is the staleness window, and does the display know it?
  • On partial failure, does the panel say how many series are missing?
  • Is error distinguishable from empty, and empty from healthy-zero?
  • Does the error say what failed and what to do, or only that something did?
  • Does the failure state survive being seen from across a room?
  • If the last known value is shown, is it labelled as such with a timestamp?
  • Does a panel failure surface anywhere other than the panel?
  • Does the alerting rule on this metric handle no-data the same way the panel does?

Compare

Grafana is two products on this question, and the gap is instructive. Its alerting side makes No Data an explicit four-way decision with a default that refuses to call silence healthy; its panel side renders a missing value as a hyphen by default. Prometheus solves the far end well with stale markers, so a departed target vanishes rather than flatlining, and leaves the five-minute middle to whoever builds the display. Datadog carries monitor status alongside the data so a degraded source can colour the panel from outside the query, rather than the panel having to infer trouble from its own empty result. Netdata makes staleness self-evident through motion: a per-second chart that stops moving is visibly stopped, which is the cheapest honest signal in this whole family.

Empty state covers valid results with no records. Loading state precedes a result or failure. Freshness indicator shows data age.

Data source badge identifies the affected source. Alert rule can notify an owner when the problem persists.

Error and stale state anatomy Four renderings of the same panel: healthy, stale but still inside the lookback window and therefore still drawing, partially failed with four of six series present, and outright broken. The middle two are the ones that look fine. Four states, two of them look like the first Healthy 6 of 6 · 4s ago Stale last sample 4m ago Partial 4 of 6 series Broken query failed 1 2 3 4 1 THREE SEPARABLE ON SIGHT This is fine, this is nothing, this is broken. A failed query has no value, and any rendering that produces a number is a lie. 2 THE DANGEROUS WINDOW Inside the lookback period a metric that stopped four minutes ago still renders and still looks current. 3 PARTIAL IS THE WORST A real chart of incomplete data is the most persuasive wrong answer a panel can give. It has to say four of six. It usually says nothing. 4 LOUD IS CORRECT Falling back to zero, holding the last value, or averaging across a gap all manufacture data. On an error panel that reads as good news. A dead target disappears from the graph. Absence is at least visible.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Three states that look alike and mean different things. A panel with no data because nothing matched, a panel showing a value nobody can vouch for, and a panel that broke. Collapsing them into one spinner is how a dashboard lies.

shadcn
npx shadcn@latest add card
Tokens
--card--muted-foreground--border--status-critical--state-stale--state-live

Healthy

6 of 6 · 4s ago

Stale

last sample 4m ago—collector is behind

Partial

4 of 6 series

Broken

query failed

PanelState.tsxA union, so a panel cannot be in an unnamed state.

/**
 * The states a panel can be in, as a closed union.
 *
 * `stale` is the one that gets lost. A panel showing a real number from twenty
 * minutes ago looks exactly like a panel showing a real number from four
 * seconds ago, and only one of them is safe to act on. Keeping the last good
 * value is right; keeping it unlabelled is not.
 */
export type PanelState =
  | { kind: "ok"; lastGoodAt: Date }
  | { kind: "loading" }
  | { kind: "empty"; because: string }
  | { kind: "stale"; lastGoodAt: Date; reason: string }
  | { kind: "partial"; have: number; of: number }
  | { kind: "error"; message: string; retry: () => void };

/** "4s ago", "4m ago". Relative, because the viewer wants age, not a clock. */
export function ago(then: Date, now: Date) {
  const s = Math.max(0, Math.round((now.getTime() - then.getTime()) / 1000));
  if (s < 60) return `${s}s ago`;
  if (s < 3600) return `${Math.round(s / 60)}m ago`;
  return `${Math.round(s / 3600)}h ago`;
}

/**
 * Every non-ok state owes the viewer a next action. An error with no retry, or
 * an empty with no explanation, is a dead end dressed as information.
 */
export function stateMessage(s: PanelState, now = new Date()): { text: string; tone: string } | null {
  switch (s.kind) {
    case "ok":
      return { text: ago(s.lastGoodAt, now), tone: "hsl(var(--muted-foreground))" };
    case "loading":
      return null;
    case "empty":
      return { text: `No data—${s.because}`, tone: "hsl(var(--muted-foreground))" };
    case "stale":
      return { text: `last sample ${ago(s.lastGoodAt, now)}—${s.reason}`, tone: "hsl(var(--state-stale))" };
    case "partial":
      return { text: `${s.have} of ${s.of} series`, tone: "hsl(var(--state-stale))" };
    case "error":
      return { text: s.message, tone: "hsl(var(--status-critical))" };
  }
}

demo.tsxHow it is called: four panels, one per state, each saying in words what its picture cannot.

import { Sparkline } from "../sparkline/Sparkline";
import { stateMessage, type PanelState } from "./PanelState";

/**
 * Four panels in four states. The middle two are the ones that look fine: a
 * stale panel still draws and a partial one still draws, so each says in
 * words what its picture cannot. The clock is fixed for the server.
 */
const NOW = new Date("2026-09-15T09:00:00Z");
const SERIES = [40, 34, 38, 22, 28, 18];

const PANELS: { title: string; state: PanelState; values: number[] }[] = [
  { title: "Healthy", state: { kind: "ok", lastGoodAt: new Date(NOW.getTime() - 4_000) }, values: SERIES },
  { title: "Stale", state: { kind: "stale", lastGoodAt: new Date(NOW.getTime() - 240_000), reason: "collector is behind" }, values: SERIES.slice(0, 5) },
  { title: "Partial", state: { kind: "partial", have: 4, of: 6 }, values: SERIES },
  { title: "Broken", state: { kind: "error", message: "query failed", retry: () => {} }, values: [] },
];

function Panel({ title, state, values }: (typeof PANELS)[number]) {
  const msg = stateMessage(state, NOW);
  const flagged = state.kind === "stale" || state.kind === "partial";
  return (
    <div className={`rounded-lg border bg-card p-3 ${flagged ? "border-2 border-state-stale" : ""}`}>
      <p className="border-b pb-1.5 text-[11px] uppercase tracking-wide text-muted-foreground">{title}</p>
      <div className="mt-2 h-11 text-muted-foreground">
        {values.length > 1 && <Sparkline values={values} width={120} height={44} />}
      </div>
      {msg && (
        <p className="mt-2 text-[10px] tabular-nums" style={{ color: msg.tone }}>
          {state.kind === "ok" ? `6 of 6 · ${msg.text}` : msg.text}
        </p>
      )}
      {state.kind === "error" && (
        <button className="mt-1 text-[10px] underline underline-offset-2" onClick={state.retry}>retry</button>
      )}
    </div>
  );
}

export default function Demo() {
  return (
    <div className="grid grid-cols-4 gap-4">
      {PANELS.map((p) => <Panel key={p.title} {...p} />)}
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

Screenshots are grouped by product and include the capture date. Dashed boxes highlight this pattern. Notes appear below each screenshot and when you hover over a box.

Grafana

Examples of panel grids, shared selectors, stat tiles, and chart configuration.

Grafana SLO / SLO Overview September 10, 2026 Grafana Play (signed out; no version string exposed) dense · dark · desktop-web
The overview lists twenty-eight objectives with an indicator, remaining budget, and sparkline for each. The budget column combines large negative percentages with positive values, but gives little explanation of how to read them. No data uses the same red as breached objectives, making missing information harder to distinguish from a failed target. The sparklines also use different scales, so their shapes cannot be compared directly across rows.
  • Metric targets The budget column adds context to each objective’s result.
  • Target and progress Large negative budget values need a clear explanation of the calculation.
  • Error and stale state No data uses the same red treatment as a breach.
  • Small multiples Sparkline scales differ between rows, limiting direct comparison.
  • Header KPI strip The top tiles count targets, objectives, and series rather than showing whether goals are met.
Show 1 more example Hide the rest

Grafana

Examples of panel grids, shared selectors, stat tiles, and chart configuration.

Linux node / fleet overview September 9, 2026 Grafana Play (signed out; no version string exposed) medium · dark · desktop-web
The data source was returning no measurements when this overview was captured. A red tile says no metrics were received and suggests checking configuration. The freshness tile says No data, while another panel says No errors. Those messages need clearer distinctions: missing measurements do not establish that a system has no errors.