Skip to content

Error handling

The fetchers draw a clean line: a missing single article is an expected null; everything else — an unseeded site, a malformed feed, a network failure — is a thrown, typed GhostwritrError. The contract fails closed: there is no API key and no authed fallback, so a bad read surfaces as an error rather than silently returning empty. See Error handling for the cross-SDK model.

  • fetchArticle({ siteId, slug }) returns null when the site’s feed exists but has no article at that slug (a 404 on the by-slug document). This is the normal “no such post” case — turn it into a 404 Response yourself:

    const article = await fetchArticle({ siteId, slug: params.slug! });
    if (!article) throw new Response("Not found", { status: 404 });
  • fetchArticles({ siteId }) never returns null. A site with a published feed but zero pages returns []; a site with no feed yet throws NOT_FOUND (below).

GhostwritrError carries a status (the HTTP status, or 0 for client-side errors), a code, and details. The codes you’ll handle:

codeWhenTypical handling
NOT_FOUNDSite has no published feed yet (manifest 404).Render an “empty blog” state, or a 404.
INVALID_RESPONSEThe feed manifest, a page, or an article was non-JSON or violated the article contract.Log + alert — this is a feed-side contract violation, not a user error.
NETWORK_ERRORThe fetch itself failed (DNS, TLS, offline). status is 0.Retry or render a transient-failure state.
CONFIGA missing siteId/slug or an invalid staticBaseUrl. status is 0.Fix the call — this is a wiring bug.
RATE_LIMITED / SERVER_ERRORThe feed origin returned 429 / 5xx.Retry with backoff; surface a transient state.

Catch GhostwritrError to branch on the code; rethrow as a Response so React Router renders the matching boundary:

app/routes/blog._index.tsx
import { fetchArticles, GhostwritrError } from "@ghostwritr/react-router";
export async function loader() {
try {
return { articles: await fetchArticles({ siteId: process.env.GHOSTWRITR_SITE_ID! }) };
} catch (err) {
if (err instanceof GhostwritrError && err.code === "NOT_FOUND") {
// No feed yet — render an empty blog rather than a 500.
return { articles: [] };
}
throw err; // INVALID_RESPONSE / NETWORK_ERROR / etc. → error boundary
}
}

GhostwritrError is re-exported from @ghostwritr/react-router (it originates in the shared @ghostwritr/feed core), and instanceof works across transpile targets. The GhostwritrErrorCode type is also re-exported if you want to narrow on it.