Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
When data required to render a page is definitively missing, do not leave a loading spinner on screen. Decide whether the condition means not found or server failure, detect it at the data-loading or route boundary, and render the matching error or not-found response. Suspense fallbacks are for content that is still pending; they are not proof that a record has failed.
Make the decision at the data boundary
A page can be in three materially different states:
| State | Meaning | Correct user outcome |
|---|---|---|
| Pending | The request or component has not finished. | Loading UI, often supplied by Suspense. |
| Not found | The requested record does not exist and that is a valid product outcome. | Not-found page and an appropriate 404 response. |
| Failure | An invariant, dependency, authorization check, or server operation failed. | Error boundary and an appropriate 4xx or 5xx response. |
Make this classification where the data is loaded, before rendering a component that assumes the value exists. A route loader is usually the clearest location because it can select an HTTP status and hand the result to the route’s error boundary.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Not-found versus server failure
- Use not found when a user requests a legitimate identifier but no record exists, such as an article slug that is not present.
- Use server failure when an invariant is broken, a required upstream service fails, or the application cannot safely determine the page state.
- Use an authorization response when the record exists but the current user is not allowed to know or view it; do not turn every permission failure into a 404 without a deliberate security reason.
React Router: throw from the loader
React Router’s documented approach is to throw response data from a loader when it cannot find what the page needs. The closest route ErrorBoundary then renders the failure UI instead of allowing a component to dereference missing data. React Router describes this as avoiding an empty page for users: “To avoid rendering an empty page to users, route modules will automatically catch errors in your code and render the closest ErrorBoundary.” See the React Router Error Boundaries guide.
#1 Best Overall
Complete route example
import {
isRouteErrorResponse,
useRouteError,
} from "react-router-dom";
export async function loader({ params }) {
const response = await fetch(`https://example.com/api/articles/${params.slug}`);
if (response.status === 404) {
throw new Response("Article not found", { status: 404 });
}
if (!response.ok) {
throw new Response("Article service failed", { status: 502 });
}
const article = await response.json();
if (!article || typeof article.title !== "string") {
throw new Response("Invalid article payload", { status: 500 });
}
return { article };
}
export function Article() {
const { article } = useLoaderData();
return {article.title}
{article.body}
;
}
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
if (error.status === 404) {
return Article not found
Check the address or return to the index.
;
}
return We could not load this article
Try again later.
;
}
return Unexpected error
;
}
In a data router, attach loader and ErrorBoundary to the route module. The loader throws before Article receives data, so the component never has to guess whether undefined means loading, absence, or corruption.
Validate more than HTTP status
A successful HTTP response can still contain an unusable payload. Check required fields and relationships at the boundary. If a missing field violates an application invariant, throw a 500-class response (or your framework’s equivalent) rather than rendering a partially empty page. Keep the user-facing message generic while logging the payload and request identifier on the server.
Why Suspense is not a missing-content detector
Suspense displays its fallback while a child suspends, then returns to the child when it is ready. A fallback therefore represents pending work, not a confirmed missing record. The React Suspense documentation states that “If a component throws an error on the server, React will not abort the server render.” In a Suspense boundary, React may emit the fallback and retry rendering on the client.
That behavior is useful for resilience, but it can hide a broken data decision if a permanently absent record is treated as a promise that never resolves. Resolve absence in the loader, server function, or route action; reserve Suspense for work that may still complete.
Choose boundary scope deliberately
A boundary around one widget can preserve the rest of a page when a recommendation panel fails. A route boundary is appropriate when the primary record is required. An application-level boundary is a last-resort shell for failures that leave no safe route-specific UI. React’s Component reference advises considering where an error message makes sense when choosing error-boundary granularity.
Server rendering: status codes and timing
The rendering API changes what the server can know before it commits a response.
Streaming with renderToReadableStream
With streaming SSR, the shell can be sent before every child finishes. React’s renderToReadableStream example tracks errors in onError and uses that state to select a 500 response. This only helps for failures observed before the response status is committed; errors after the shell is sent cannot retroactively change an HTTP status.
Recommended Free Tools
import { renderToReadableStream } from "react-dom/server";
export async function handle(request) {
let didError = false;
const stream = await renderToReadableStream( , {
onError(error) {
didError = true;
console.error(error);
},
});
return new Response(stream, {
status: didError ? 500 : 200,
headers: { "content-type": "text/html; charset=utf-8" },
});
}
Put critical data loading where the server can observe its outcome before committing a status. If a required record is absent, perform the lookup before starting the stream or have the framework’s route data layer represent the 404 explicitly.
Rank #3
String rendering with renderToString
renderToString does not wait for suspended content. It emits the nearest Suspense fallback. That is appropriate when a fallback is acceptable in the initial HTML, but it does not provide a “wait until required data exists” guarantee. Do not infer success from the presence of generated HTML.
Static output with prerender
For builds that must wait for suspended content before producing static HTML, use a data-loading path designed to wait, such as React’s documented static prerender API where your framework supports it. If required content is unavailable during the build, fail the build or produce an intentional not-found artifact according to the site’s publishing rules; do not silently ship a permanent loading shell.
Failure handling checklist
- Define which fields are required for the route’s primary UI.
- Fetch and validate them in the loader or server data function.
- Map an absent record to a deliberate 404/not-found response.
- Map broken invariants and dependency failures to an error response.
- Render the nearest boundary with an actionable, accessible message.
- Set the HTTP status before streaming commits headers.
- Log technical details server-side without exposing secrets or payloads to users.
- Test pending, not-found, malformed, unauthorized, timeout, and upstream-error paths separately.
Common mistakes and fixes
Showing a spinner forever
Cause: a missing record is represented as an unresolved promise or a state value that the UI treats as loading. Fix: return or throw a typed not-found result from the data boundary.
Rendering empty markup with a 200 status
Cause: the component uses optional chaining and quietly renders nothing. Fix: validate required data before rendering and select 404 or 5xx semantics explicitly.
Rank #4
Changing a streamed response to 404 too late
Cause: headers were committed before the loader failure surfaced. Fix: resolve route-critical data before streaming, or accept that a client-side boundary can change visible UI but not the already-sent status.
Assuming renderToString waits
Cause: confusing a server-rendered fallback with completed content. Fix: use a waiting prerender/data path when static HTML must include the required content.
One global boundary for every error
Cause: boundary placement was chosen for convenience. Fix: place boundaries where the corresponding message makes sense: widget, route, or application shell.
Or skip the browser setup
If you need screenshots of the resulting not-found or error pages, ScreenshotNeo provides a single HTTP request instead of maintaining browser automation. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.
Use the API shown in the ScreenshotNeo documentation:
Best Value
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Equivalent Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Equivalent Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Should every missing record be a 404?
No. A genuinely absent public resource is usually 404; an invariant violation or dependency failure is a server error. Choose based on product semantics and the information you can safely reveal.
Can an ErrorBoundary replace server status handling?
No. It controls rendered UI. The server must still determine the HTTP status before headers are committed, especially for streamed responses.
Is a Suspense fallback appropriate for a slow API?
Yes, when the request is still expected to complete. Once the application knows the required record cannot exist or cannot be used, leave the pending state and throw or return the deliberate failure.
Frequently Asked Questions
Should every missing record be a 404?
No. A genuinely absent public resource is usually 404; an invariant violation or dependency failure is a server error.
Can an ErrorBoundary replace server status handling?
No. It controls rendered UI; the server must determine the HTTP status before headers are committed.
Free tools Windows power users keep installed
One-click scans. No signup required.
Is a Suspense fallback appropriate for a slow API?
Yes, while the request is expected to complete. Definitive absence should become a deliberate not-found or error result.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

