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 to use
Section titled “When to use”- 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.
Live example
Section titled “Live example”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.
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.
The reports did not load. Check your connection and try again. | |
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.
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.
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. |
Behaviour
Section titled “Behaviour”- A section fails. The error shows only in that section; the rest of the page is unaffected.
- The user presses “Try again”. The button loads and keeps focus.
- 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.
- On failure the error stays in place and the button is usable again.
- 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.
- At page level,
ResultStategives 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.
Accessibility
Section titled “Accessibility”- An error that appears later is an
InlineAlertwithannounce,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
ResultStateheading carries the same prefix. - The error code is selectable text; users can copy it and send it to support.
Content
Section titled “Content”- 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”.
Motion
Section titled “Motion”| 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.
Components used
Section titled “Components used”Card, InlineAlert, Banner, ResultState, Toast, DataTable, Button, Link. Related patterns: Loading, Notifications, Forms and validation.