500 server error block

Retry SvButton with loading that alternates fail and succeed, each with a toast. Also shows a request id with a Copy button, the failure time and a status link; SvBadge flips to Resolved.

A live, editable Svelte 5 component example from the SvGrid gallery (Blocks). Read the SvGrid UI component docs for the full API.

About this example

A 500 server error page block for Svelte 5: a Retry SvButton that shows loading and reports the result in a toast, a request id with a copy SvButton for support tickets, the time of the failure and a status-page link. On success the SvBadge flips to Resolved and the card turns green; the demo alternates fail and succeed so both paths show.

500 server error block - a "something broke on our side" screen with a Retry SvButton that shows loading and reports the outcome in a toast, a request id the user can copy for support (copy SvButton -> toast), and a link to the status page. SvBadge shows the incident state. Without a retry prop the demo alternates fail / succeed so both paths are visible. Pure UI-kit, --sg-* themed.

Headless: createServerError() below owns the retry flow, the attempt count, the failure time and copying the request id, with no markup. The component under it is one styled render of that core; import createServerError from this file (or npx @svgrid/ui add error-pages-500 --headless for the core alone) to draw your own.

Imports, features and API used

Imports: @svgrid/grid

Frequently asked questions

How do I make Retry do real work?

Pass a retry prop that returns a promise, for example one that re-requests the page, throws if it still fails, then calls invalidateAll from $app/navigation. If it resolves the block shows the recovered state and a success toast; if it rejects it shows an error toast. Without the prop a simulated retry fails first and succeeds next.

Where does the request id come from?

It is the requestId prop, which reaches createServerError through a getter. In SvelteKit return an id from handleError in hooks.server.ts and pass page.error.errorId to the block; an empty string hides the row. The core's copyId() writes it with navigator.clipboard and reports the result to onCopy, which toasts it, or a warning if the browser blocks it.

How does the layout change on a phone?

Below 560 px the icon moves above the text, the card padding shrinks and the request id and time stack in one column.

Related documentation

Related articles

Source code (531-block-error-pages-500.svelte)

<script lang="ts" module>
  /**
   * 500 server error block - a "something broke on our side" screen with a
   * Retry SvButton that shows loading and reports the outcome in a toast, a
   * request id the user can copy for support (copy SvButton -> toast), and a
   * link to the status page. SvBadge shows the incident state. Without a
   * `retry` prop the demo alternates fail / succeed so both paths are visible.
   * Pure UI-kit, --sg-* themed.
   *
   * Headless: createServerError() below owns the retry flow, the attempt
   * count, the failure time and copying the request id, with no markup. The
   * component under it is one styled render of that core; import
   * createServerError from this file (or `npx @svgrid/ui add error-pages-500
   * --headless` for the core alone) to draw your own.
   */
  export type ServerErrorOptions = {
    /** Shown for support. Pass '' to hide it. Read lazily (getter-friendly). */
    requestId?: string
    /** Status page link. Read lazily (getter-friendly). */
    statusUrl?: string
    /** Re-run the failed request; reject to stay on the error. */
    retry?: () => Promise<unknown>
    onRecovered?: (attempt: number) => void
    onRetryFailed?: (error: unknown) => void
    /** Clipboard write; defaults to navigator.clipboard.writeText. */
    writeClipboard?: (text: string) => Promise<void>
    onCopy?: (ok: boolean, requestId: string) => void
  }

  export function createServerError(opts: ServerErrorOptions = {}) {
    // requestId and statusUrl are read on every access; getters in opts keep props live.
    const requestId = () => opts.requestId ?? 'req_8f3a21c9e04b'
    const failedAt = new Date().toISOString().slice(0, 19).replace('T', ' ') + ' UTC'
    let busy = $state(false)
    let recovered = $state(false)
    let attempts = $state(0)

    async function retry() {
      if (busy) return
      busy = true
      try {
        await opts.retry?.()
        recovered = true
        opts.onRecovered?.(attempts + 1)
      } catch (err) {
        opts.onRetryFailed?.(err)
      } finally {
        attempts += 1
        busy = false
      }
    }

    async function copyId() {
      const id = requestId()
      try {
        await (opts.writeClipboard ?? ((t: string) => navigator.clipboard.writeText(t)))(id)
        opts.onCopy?.(true, id)
      } catch {
        opts.onCopy?.(false, id)
      }
    }

    return {
      get requestId() { return requestId() },
      get statusUrl() { return opts.statusUrl ?? 'https://status.example.com' },
      failedAt,
      get busy() { return busy },
      get recovered() { return recovered },
      get attempts() { return attempts },
      retry,
      copyId,
      /** Spread on your Retry <button>. */
      retryProps: () => ({ type: 'button' as const, disabled: busy, 'aria-busy': busy, onclick: retry }),
    }
  }
</script>

<script lang="ts">
  import { SvButton, SvBadge, SvToaster, toast } from '@svgrid/grid'

  let { requestId, retry, statusUrl }: { requestId?: string; retry?: () => Promise<unknown>; statusUrl?: string } = $props()
  // Pass an empty requestId to hide the id row.

  // Demo stand-in for `retry`: the first try fails, the next one succeeds.
  let tries = 0
  const simulate = () =>
    new Promise<void>((res, rej) => setTimeout(() => (tries++ % 2 ? res() : rej(new Error('503'))), 1000))

  const srv = createServerError({
    get requestId() { return requestId },
    get statusUrl() { return statusUrl },
    retry: () => (retry ?? simulate)(),
    onRecovered: () => toast.success('Back online. Your report loaded.'),
    onRetryFailed: () => toast.error('Still failing. Give it a minute, or send us the request id.'),
    onCopy: (ok, id) =>
      ok ? toast.success(`Copied ${id}`) : toast.warning('Copy blocked by the browser. Select the id and copy it by hand.'),
  })
</script>

<main class="sv-srv">
  <div class="sv-srv-card" class:sv-srv-ok={srv.recovered}>
    <span class="sv-srv-icon" aria-hidden="true">
      {#if srv.recovered}
        <svg viewBox="0 0 24 24" width="24" height="24"><path d="m5 12.5 4.5 4.5L19 7.5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" /></svg>
      {:else}
        <svg viewBox="0 0 24 24" width="24" height="24"><path d="M3 6.5h18v11H3zM7 10l2 2-2 2M12 14h4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /></svg>
      {/if}
    </span>

    <div class="sv-srv-main">
      <SvBadge variant={srv.recovered ? 'success' : 'danger'} dot>
        {srv.recovered ? 'Resolved' : '500 - Internal server error'}
      </SvBadge>
      <h2>{srv.recovered ? 'We are back' : 'Something broke on our side'}</h2>
      <p class="sv-srv-muted">
        {#if srv.recovered}
          The reporting service answered on attempt {srv.attempts}. Nothing you entered was lost.
        {:else}
          Acme Analytics could not build this report. Your data is safe; the
          reporting service did not answer in time. Retrying usually works.
        {/if}
      </p>

      <div class="sv-srv-actions">
        {#if srv.recovered}
          <SvButton variant="primary" href="/">Continue to reports</SvButton>
        {:else}
          <SvButton variant="primary" loading={srv.busy} onclick={srv.retry}>{srv.busy ? 'Retrying...' : 'Retry'}</SvButton>
        {/if}
        <a class="sv-srv-status" href={srv.statusUrl} target="_blank" rel="noreferrer">
          View status page
          <svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"><path d="M6 3h7v7M13 3 4 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" /></svg>
        </a>
      </div>

      <dl class="sv-srv-meta">
        {#if srv.requestId}
          <div>
            <dt>Request id</dt>
            <dd class="sv-srv-rid">
              <code>{srv.requestId}</code>
              <SvButton variant="ghost" size="sm" ariaLabel="Copy request id" onclick={srv.copyId}>
                {#snippet icon()}
                  <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M5.5 5.5h7v8h-7zM3.5 10.5v-8h7" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" /></svg>
                {/snippet}
                Copy
              </SvButton>
            </dd>
          </div>
        {/if}
        <div><dt>Time</dt><dd>{srv.failedAt}</dd></div>
      </dl>
    </div>
  </div>
</main>

<SvToaster position="bottom-right" />

<style>
  .sv-srv {
    display: grid;
    place-items: center;
    min-height: 540px;
    padding: 40px 24px;
    box-sizing: border-box;
    color: var(--sg-fg, #0f172a);
  }
  .sv-srv-card {
    --_tone: var(--sg-danger, #dc2626);
    display: flex;
    gap: 20px;
    width: 100%;
    max-width: 600px;
    padding: 28px;
    box-sizing: border-box;
    background: var(--sg-bg, #fff);
    border: 1px solid var(--sg-border, #e2e8f0);
    border-top: 3px solid var(--_tone);
    border-radius: var(--sg-radius-lg, 12px);
  }
  .sv-srv-card.sv-srv-ok { --_tone: var(--sg-success, #16a34a); }
  .sv-srv-icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 48px;
    height: 48px;
    border-radius: 12px;
    color: var(--_tone);
    background: color-mix(in srgb, var(--_tone) 12%, transparent);
  }
  .sv-srv-main { flex: 1; min-width: 0; }
  h2 { margin: 12px 0 8px; font-size: 24px; letter-spacing: -.015em; }
  .sv-srv-muted { margin: 0; color: var(--sg-muted, #64748b); font-size: 14.5px; line-height: 1.55; }
  .sv-srv-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; margin-top: 22px; }
  .sv-srv-status { display: inline-flex; align-items: center; gap: 5px; font-size: 13.5px; font-weight: 500; color: var(--sg-fg, #0f172a); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--sg-border, #cbd5e1); }
  .sv-srv-status:hover { text-decoration-color: currentColor; }
  .sv-srv-meta { display: grid; grid-template-columns: 1fr auto; gap: 16px; margin: 24px 0 0; padding-top: 16px; border-top: 1px solid var(--sg-border, #e2e8f0); }
  dt { font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--sg-muted, #64748b); }
  dd { margin: 6px 0 0; font-size: 13px; font-variant-numeric: tabular-nums; }
  .sv-srv-rid { display: flex; align-items: center; gap: 6px; min-width: 0; }
  code { font-size: 13px; padding: 3px 7px; border-radius: 6px; background: color-mix(in srgb, var(--sg-fg, #0f172a) 7%, var(--sg-bg, #fff)); overflow-wrap: anywhere; }

  @media (max-width: 560px) {
    .sv-srv { padding: 24px 12px; }
    .sv-srv-card { flex-direction: column; gap: 14px; padding: 22px 18px; }
    h2 { font-size: 21px; }
    .sv-srv-meta { grid-template-columns: 1fr; }
  }
</style>

View this example on GitHub

More Blocks examples

  • Login block - A two-column authentication page: a branded gradient panel beside a sign-in SvCard with social SvButtons, SvTextInput + SvPasswordInput (reveal), remember-me SvCheckBox, an "or" SvDivider and forgot-password link. Drops into any route as-is.
  • Sign-up block - A centered account-creation card: two-up name fields, work email, a password with the live strength meter, a plan SvSegmented switch, terms SvCheckBox and a benefits rail. Submit fires a promise toast. Pure UI-kit.
  • Two-factor / OTP block - The verification step of a sign-in flow: SvOtpInput (6 digits, auto-advance + onComplete), a resend cooldown timer, and the waiting -> verifying -> verified states. Enter 000000 to see the error path.
  • Analytics dashboard block - A full overview page: a range SvSegmented + actions header, a KPI grid (SvStat + SvSparkline), a revenue bar panel, a recent-activity SvTimeline and a top-products list with SvProgress bars. No grid dependency.
  • App shell block - A complete application layout: a SvNavPane sidebar with sections + badges, a top bar with SvBreadcrumb, search and an account SvMenu, and a routed content area. Nav swaps the page; the toggle collapses the rail to an icon rail.