plugins/frontend-toolkit/skills/async-ux-states/SKILL.md
Ensure every async view handles loading, error, and empty states correctly with proper Suspense boundaries and custom 404/500 pages. Use when QA reports blank screens, infinite spinners, or undefined exposure, or before shipping. Not for choosing route render strategy (use render-strategy-decision) or form-specific error handling (use form-ux).
npx skillsauth add jaykim88/claude-ai-engineering async-ux-statesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Every asynchronous view in the app handles the three states (loading, error, empty) completely. No white screens, no undefined exposed to users, no untrapped errors.
Universal — the 3-state pattern (loading / error / empty), custom 404/500 pages, Error Boundary placement, and Suspense-boundary granularity exist in every modern meta-framework. Only the file-naming conventions and component APIs differ.
Audit loading states
fetch, useQuery, useSWR, Server Components)api-caching-optimization)aria-busy while loading; give skeletons an SR-only "Loading…" labelAudit error states — match the action to the error type
role="alert" / aria-live region and move focus to it (keyboard / SR users)Audit empty states
Place error/loading boundary files at the correct route segment
Customize 404 and 500 pages
Suspense boundary granularity
Error Boundary fallback UI
Verify (validation loop)
[], throw) and confirm the right UI — never a blank screen, undefined, or infinite spinner| Tier | Examples | Action SLA |
|---|---|---|
| Critical | Blank/white screen or raw undefined shown; an unhandled error crashes the whole route; infinite spinner (no timeout) | Block release; fix immediately |
| Major | Async view missing an error or empty state; generic "Retry" shown for a 401/403/404; stack trace exposed to users; error not announced to screen readers | Fix this sprint |
| Minor | Spinner flash on fast responses; skeleton size mismatch (CLS); empty state without a CTA | Schedule within 2 sprints |
role="alert"/aria-live) and focusableerror.tsx placed at correct segment (not same as throwing layout)loading.tsx, error.tsx, not-found.tsx, global-error.tsx placed at correct segmentsfeat(error-ux): add 3-state to <view> or fix(error-ux): correct error.tsx placement at <segment>loading.tsx / error.tsx / not-found.tsx / global-error.tsxerror.tsx cannot catch errors in its same-segment layout.tsx — place at parent segment; for root layout, only global-error.tsx catchesreset (or unstable_retry in Next.js 16.2+) prop passed to error.tsxErrorBoundary, or unstable_catchError from next/error (Next.js 16.2+, API may change) — unstable_catchError(fallback) returns a wrapper; fallback receives (props, { error, unstable_retry, reset })AbortSignal.timeout(ms) on fetch; TanStack Query retry + retryDelay (exponential), placeholderData: keepPreviousData to avoid blanking on refetchanimation-delay so the spinner only appears if the fetch is actually slow<Suspense fallback={...}> boundarieserror.vue for global errors; <NuxtErrorBoundary> for component-level; <Suspense> for async components; useFetch returns error ref for inline error state+error.svelte at any route level catches errors from load() and child components; +loading.svelte for streaming; goto('/') for retry after fixErrorHandler injectable for uncaught errors; route-level resolve() errors handled by Router events; *ngIf patterns for empty statesrender-strategy-decision — Suspense placement is part of route strategyobservability-setup — error UI must coordinate with Sentry error captureform-ux — form-specific errors handled thereerror.tsx does NOT catch errors thrown in the same-segment layout.js — place error.tsx at the parent segment. Co-locate loading.tsx at the smallest subtree that does data fetching so Suspense fallbacks stay granular instead of blanking entire routes. Match the recovery action to the error type — a generic "Retry" is wrong for 401/403/404. An unresolved fetch is an infinite spinner (a white screen by another name) — always set a timeout.development
Design webhooks correctly on both sides — sending (HMAC signing, retries with backoff, at-least-once) and receiving (verify signature on raw body, enqueue + 200 fast, dedupe on event id). Use when adding webhook delivery or consuming a provider's webhooks. Not for internal service-to-service events (use async-messaging) or general outbound-call retry policy (use resilience-patterns).
testing
Use transactions and isolation levels correctly — keep them short, no network calls inside, explicit isolation, retry on serialization conflicts, and choose optimistic vs pessimistic locking. Use when a write spans multiple tables, when concurrent updates corrupt data, or when designing money/inventory flows. Not for cross-service event delivery (use async-messaging Outbox) or schema-level constraints (use schema-design).
development
Backend testing pyramid — unit for pure logic, integration against a real DB (Testcontainers), and consumer-driven contract testing (Pact) for service boundaries. Use before a feature, after a bug fix, or when services break each other on deploy. Not for load testing (use performance-profiling) or security testing (use backend-security-audit).
data-ai
Design a relational schema — normalize to 3NF then denormalize with justification, choose the right Postgres index type per data shape, enforce constraints at the DB. Use when modeling a new domain, when queries are slow, or before a migration. Not for diagnosing slow queries (use query-optimization) or shipping the change without downtime (use migration-strategy).