Error handling
There is exactly one error type across every SDK: GhostwritrError. It is always catchable, always inspectable, and the same class everywhere — so a single instanceof check works whether you imported from @ghostwritr/feed, @ghostwritr/next, @ghostwritr/astro, or @ghostwritr/react-router.
GhostwritrError
Section titled “GhostwritrError”class GhostwritrError extends Error { readonly status: number; // HTTP status, or 0 for client-side readonly code: GhostwritrErrorCode | null; readonly details?: unknown; // the offending URL or underlying cause}statusis the HTTP status when the failure came from a response (e.g.429,500). It is0for anything that never reached a server: config, network, or a parse failure.codeis a stable, switchable identifier (below). Prefer it overstatus.detailscarries context for debugging — often the URL that failed or the underlying networkcause. Don’t surface it to users.
instanceof GhostwritrError is preserved across transpile targets, so it works regardless of your build.
The code union
Section titled “The code union”type GhostwritrErrorCode = | "CONFIG" | "NETWORK_ERROR" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "RATE_LIMITED" | "SERVER_ERROR" | "INVALID_RESPONSE" | (string & {});| Code | status | Means |
|---|---|---|
CONFIG | 0 | A bad call — missing siteId, missing slug, or an invalid staticBaseUrl. A bug to fix, not retry. |
NETWORK_ERROR | 0 | The fetch itself threw (DNS, offline, TLS). The request never got a response. |
NOT_FOUND | 404 | The feed (or a requested object) isn’t there. For a site whose feed hasn’t been built yet, this is what you get. |
RATE_LIMITED | 429 | Throttled — back off and retry. |
UNAUTHORIZED | 401 | Mapped from status; not expected on the keyless read path. |
FORBIDDEN | 403 | Mapped from status; not expected on the keyless read path. |
SERVER_ERROR | ≥500 | The feed origin returned a server error. |
INVALID_RESPONSE | varies | The response wasn’t valid JSON, or violated the article/feed contract (a guaranteed field missing or mistyped). |
Fail closed
Section titled “Fail closed”The keyless read path has no authed fallback — so when something is wrong, the SDK throws rather than silently returning empty or stale data. Two cases are worth telling apart:
NOT_FOUND— the feed isn’t there yet. A site that hasn’t been backfilled has no manifest, so the read fails closed withNOT_FOUND. This is the expected, recoverable state for a brand-new site. Treat it as “nothing published yet”: render an empty state, don’t crash the build. (NotefetchArticle({ slug })resolves a missing single article tonullrather than throwing —NOT_FOUNDis for an absent feed/manifest, not an absent slug.)INVALID_RESPONSE— the contract was violated. The feed responded, but the payload was non-JSON, the manifest was malformed, or a published article was missing a guaranteed field. A complete article is the contract, so this is never coerced — it surfaces loudly. This points at a real problem, not an empty site.
import { GhostwritrError } from "@ghostwritr/feed";
try { const articles = await fetchArticles({ siteId }); return render(articles);} catch (err) { if (err instanceof GhostwritrError) { if (err.code === "NOT_FOUND") return renderEmptyState(); // nothing published yet if (err.code === "RATE_LIMITED") return retryLater(); // INVALID_RESPONSE, SERVER_ERROR, NETWORK_ERROR, CONFIG, … } throw err;}That distinction is the whole philosophy: a missing feed is a normal, handled state (NOT_FOUND); a broken feed is a hard failure you want to see (INVALID_RESPONSE). Nothing in between is papered over.