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.
null vs. throw
Section titled “null vs. throw”-
fetchArticle({ siteId, slug })returnsnullwhen 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 404Responseyourself:const article = await fetchArticle({ siteId, slug: params.slug! });if (!article) throw new Response("Not found", { status: 404 }); -
fetchArticles({ siteId })never returnsnull. A site with a published feed but zero pages returns[]; a site with no feed yet throwsNOT_FOUND(below).
The error codes
Section titled “The error codes”GhostwritrError carries a status (the HTTP status, or 0 for client-side errors), a code, and details. The codes you’ll handle:
code | When | Typical handling |
|---|---|---|
NOT_FOUND | Site has no published feed yet (manifest 404). | Render an “empty blog” state, or a 404. |
INVALID_RESPONSE | The 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_ERROR | The fetch itself failed (DNS, TLS, offline). status is 0. | Retry or render a transient-failure state. |
CONFIG | A missing siteId/slug or an invalid staticBaseUrl. status is 0. | Fix the call — this is a wiring bug. |
RATE_LIMITED / SERVER_ERROR | The feed origin returned 429 / 5xx. | Retry with backoff; surface a transient state. |
Catching it in a loader
Section titled “Catching it in a loader”Catch GhostwritrError to branch on the code; rethrow as a Response so React Router renders the matching boundary:
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.