Skip to content
BigBang

Popular

    ↑↓Navigate↵OpenEscClose
    TR

    Patterns

    Error recovery

    What to show for each kind of error, how to keep user input and how to guide users onwards.

    Errors are unavoidable; leaving users in a dead end is not. This pattern sets what to say for each kind of error, what to keep and how users continue.

    • When a field, section, action, connection or page fails.
    • When a load fails and the user must try again.
    • When a page is not found or the server returns an error.

    The card fails on first load. A danger InlineAlert stays inside the card; the rest of the page works. Press “Try again”: the button loads, then the content arrives and a “Loaded” toast appears.

    Live example, StorybookOpen in Storybook

    Recent reports

    Error: Not loaded

    The reports did not load. The rest of the page keeps working.
    import { Button, Card, DescriptionList, InlineAlert, useToast } from '@bigbang/ui';
    import { useEffect, useRef, useState } from 'react';
    import { type Lang, LangProvider } from '../lang';
    const copy = {
    tr: {
    title: 'Son raporlar',
    errorTitle: 'Yüklenemedi',
    error: 'Raporlar yüklenemedi. Sayfanın geri kalanı çalışmaya devam ediyor.',
    retry: 'Tekrar dene',
    retrying: 'Yükleniyor',
    loaded: 'Yüklendi',
    items: [
    { term: 'Rapor 1', details: '12 kayıt' },
    { term: 'Rapor 2', details: '250 kayıt' },
    { term: 'Rapor 3', details: '1.000 kayıt' },
    ],
    },
    en: {
    title: 'Recent reports',
    errorTitle: 'Not loaded',
    error: 'The reports did not load. The rest of the page keeps working.',
    retry: 'Try again',
    retrying: 'Loading',
    loaded: 'Loaded',
    items: [
    { term: 'Report 1', details: '12 records' },
    { term: 'Report 2', details: '250 records' },
    { term: 'Report 3', details: '1,000 records' },
    ],
    },
    };
    type State = 'error' | 'loading' | 'ready';
    type Props = { lang?: Lang; delay?: number };
    function Section({ lang = 'tr', delay = 1200 }: Props) {
    const t = copy[lang];
    const toast = useToast();
    const [state, setState] = useState<State>('error');
    const bodyRef = useRef<HTMLDivElement>(null);
    const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
    const hadFocusRef = useRef(false);
    useEffect(() => () => clearTimeout(timerRef.current), []);
    // The retry button leaves the page on success. If it held focus, hand it to the card body instead of dropping it.
    useEffect(() => {
    if (state === 'ready' && hadFocusRef.current) {
    hadFocusRef.current = false;
    bodyRef.current?.focus({ preventScroll: true });
    }
    }, [state]);
    const retry = () => {
    if (state === 'loading') return;
    hadFocusRef.current = true;
    setState('loading');
    timerRef.current = setTimeout(() => {
    setState('ready');
    toast.show({ title: t.loaded, tone: 'success' });
    }, delay);
    };
    return (
    <Card className="w-full max-w-md">
    <Card.Header>
    <Card.Title>{t.title}</Card.Title>
    </Card.Header>
    <Card.Body>
    <div ref={bodyRef} tabIndex={-1} className="outline-none">
    {state === 'ready' ? (
    <DescriptionList items={t.items} />
    ) : (
    <InlineAlert
    tone="danger"
    title={t.errorTitle}
    actions={
    <Button variant="secondary" size="sm" loading={state === 'loading'} loadingLabel={t.retrying} onClick={retry}>
    {t.retry}
    </Button>
    }
    >
    {t.error}
    </InlineAlert>
    )}
    </div>
    </Card.Body>
    </Card>
    );
    }
    /**
    * A section whose first load failed (simulated). A danger `InlineAlert` stays inside the card, with a retry action.
    * The button keeps focus while it retries; on success the card body takes focus once and a success toast says "Loaded".
    */
    export function SectionRecovery(props: Props) {
    return (
    <LangProvider lang={props.lang ?? 'tr'}>
    <Section {...props} />
    </LangProvider>
    );
    }

    The table shows a message and a retry button in its error state.

    Live example, StorybookOpen in Storybook
    Report list
    import { createDataTableColumnHelper, DataTable } from '@bigbang/ui';
    import { useEffect, useMemo, useRef, useState } from 'react';
    import { type Lang, LangProvider } from '../lang';
    const copy = {
    tr: {
    caption: 'Rapor listesi',
    name: 'Ad',
    count: 'Kayıt',
    error: 'Raporlar yüklenemedi. Bağlantınızı denetleyip tekrar deneyin.',
    retry: 'Tekrar dene',
    item: 'Rapor',
    },
    en: {
    caption: 'Report list',
    name: 'Name',
    count: 'Records',
    error: 'The reports did not load. Check your connection and try again.',
    retry: 'Try again',
    item: 'Report',
    },
    };
    type Row = { id: string; name: string; count: number };
    const helper = createDataTableColumnHelper<Row>();
    const counts = [12, 250, 1000, 48];
    /** A table whose load failed: `status="error"` shows the message and a retry button; retry shows the loading rows, then the data. */
    export function TableRecovery({ lang = 'tr', delay = 1200 }: { lang?: Lang; delay?: number }) {
    const t = copy[lang];
    const [status, setStatus] = useState<'error' | 'loading' | 'ready'>('error');
    const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
    useEffect(() => () => clearTimeout(timerRef.current), []);
    const columns = useMemo(
    () => [
    helper.accessor('name', { header: t.name, meta: { rowHeader: true } }),
    helper.accessor('count', { header: t.count, meta: { numeric: true } }),
    ],
    [t],
    );
    const data = useMemo<Row[]>(() => counts.map((count, index) => ({ id: `r${index + 1}`, name: `${t.item} ${index + 1}`, count })), [t]);
    const retry = () => {
    setStatus('loading');
    timerRef.current = setTimeout(() => setStatus('ready'), delay);
    };
    return (
    <LangProvider lang={lang}>
    <div className="w-full max-w-xl">
    <DataTable
    columns={columns}
    data={status === 'ready' ? data : []}
    getRowId={(row) => row.id}
    caption={t.caption}
    status={status}
    onRetry={retry}
    messages={{ error: t.error, retry: t.retry }}
    />
    </div>
    </LangProvider>
    );
    }

    A page-level error says what happened, gives an error code and links to the home page.

    Live example, StorybookOpen in Storybook

    Error: Something went wrong

    We cannot open this page right now. Try again in a moment; if it continues, send the code below to support.

    Error code: 5F2A
    import { Button, Link, ResultState, Text } from '@bigbang/ui';
    import { useEffect, useRef, useState } from 'react';
    import { type Lang, LangProvider } from '../lang';
    const copy = {
    tr: {
    'not-found': { title: 'Sayfa bulunamadı', text: 'Aradığınız sayfa taşınmış veya silinmiş olabilir. Adresi denetleyin ya da ana sayfaya dönün.' },
    server: { title: 'Bir sorun oluştu', text: 'Sayfayı şu an açamıyoruz. Biraz sonra yeniden deneyin; sorun sürerse destek ekibine aşağıdaki kodu iletin.' },
    code: 'Hata kodu',
    retry: 'Tekrar dene',
    retrying: 'Yükleniyor',
    home: 'Ana sayfaya dön',
    },
    en: {
    'not-found': { title: 'Page not found', text: 'The page may have moved or been removed. Check the address or go back to the home page.' },
    server: { title: 'Something went wrong', text: 'We cannot open this page right now. Try again in a moment; if it continues, send the code below to support.' },
    code: 'Error code',
    retry: 'Try again',
    retrying: 'Loading',
    home: 'Go to the home page',
    },
    };
    /**
    * A page-level error with `ResultState` (tone danger): what happened, what to do, a reference code for support
    * and a way home. A server error also offers a retry (simulated: it stays loading for `delay` ms, then fails again).
    * The heading is level 1 because the demo is a whole page.
    */
    export function PageError({
    lang = 'tr',
    kind = 'server',
    code = '5F2A',
    delay = 1200,
    }: {
    lang?: Lang;
    kind?: 'not-found' | 'server';
    code?: string;
    delay?: number;
    }) {
    const t = copy[lang];
    const message = t[kind];
    const [retrying, setRetrying] = useState(false);
    const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
    useEffect(() => () => clearTimeout(timerRef.current), []);
    const retry = () => {
    setRetrying(true);
    timerRef.current = setTimeout(() => setRetrying(false), delay);
    };
    return (
    <LangProvider lang={lang}>
    <div className="w-full max-w-lg">
    <ResultState
    tone="danger"
    headingLevel={1}
    title={message.title}
    description={message.text}
    actions={
    kind === 'server' ? (
    <>
    <Button loading={retrying} loadingLabel={t.retrying} onClick={retry}>
    {t.retry}
    </Button>
    <Link href="/">{t.home}</Link>
    </>
    ) : (
    <Link href="/">{t.home}</Link>
    )
    }
    >
    {kind === 'server' ? (
    <Text size="sm" tone="subtle">
    {t.code}: {code}
    </Text>
    ) : null}
    </ResultState>
    </div>
    </LangProvider>
    );
    }
    Error kind What to do
    Field error Under the field, with how to fix it. Details: Forms and validation.
    Section load failure The rest of the page keeps working. The section shows a danger InlineAlert with a “Try again” action.
    Action failure Keep the user’s input. Say what happened and what to do, and offer a retry.
    Connection lost or timeout Retry automatically with backoff at 1 s, 2 s, 4 s; then manual “Try again”.
    Partial failure The page works but part of it is missing: a warning InlineAlert; a Banner when the whole page is affected.
    Page level ResultState (danger): not found or server error; a reference code such as “Error code: 5F2A”, “Try again” and a link to the home page.
    Focus Stays on the retry button while retrying; does not jump to the top after success.
    Announcement An error that appears later is role="alert"; a successful recovery is a polite “Loaded” toast.
    The reports did not load.
    DoThe message says what happened and offers an action.
    An error occurred.
    Don'tA message without an action leaves users in a dead end.
    1. A section fails. The error shows only in that section; the rest of the page is unaffected.
    2. The user presses “Try again”. The button loads and keeps focus.
    3. On success the content arrives and a “Loaded” toast is announced politely. If the button held focus, focus moves to the section body; the page does not scroll.
    4. On failure the error stays in place and the button is usable again.
    5. When the connection is lost, automatic retries run at 1 s, 2 s and 4 s; after the third failure only a manual retry remains.
    6. At page level, ResultState gives users the reason the page cannot open, a reference code, a “Try again” button and a link to the home page.

    When an action fails, entered text and selections are never cleared. For the save flow see the Saving pattern.

    • An error that appears later is an InlineAlert with announce, role="alert"; screen readers read it once. An error that comes with the page is not announced; it is part of the page.
    • The “Try again” button keeps focus while loading; a loading Button stays focusable.
    • After success, the “Loaded” toast is announced politely.
    • The error comes with an icon and a hidden “Error:” prefix; colour never carries the meaning alone. The ResultState heading carries the same prefix.
    • The error code is selectable text; users can copy it and send it to support.
    • Say what happened and what to do: “The reports did not load. Check your connection and try again.”
    • Avoid vague wording such as “An error occurred” and avoid blame.
    • Buttons start with a verb: “Try again”.
    • The reference code is short and fixed in form: “Error code: 5F2A”.
    Item Motion Preset
    Inline error Fade on dismiss motion.collapsible
    Retry button 0.97 scale on press; spinner while loading motion.press
    Content arrival 110 ms fade motion.reveal

    With reduced motion the shifts are removed and only a short fade stays.

    Card, InlineAlert, Banner, ResultState, Toast, DataTable, Button, Link. Related patterns: Loading, Notifications, Forms and validation.