Show navigationHide navigation
Error handling
OctoCMS layers error coverage in two places:
- Route-level
error.tsx— one per admin segment, rendering a sharedAdminErrorView. Catches anything thrown during render of that segment'spage.tsx. - Component-level
<ErrorBoundary>— wraps high-risk client components so a single component crash doesn't blank the whole route.
Both reuse the same parseContentSourceFromMessage logic from octocms/lib/contentSourceError.ts so GitHub-specific copy is consistent across the admin and the public site.
Route-level boundaries (error.tsx)#
Each per-segment route under src/app/cms/ ships an error.tsx:
cms/error.tsx— admin-wide fallbackcms/content/error.tsx— "Couldn't load content"cms/content/[type]/[id]/error.tsx— "Couldn't load this entry"cms/media/error.tsx— "Couldn't load media"cms/model/error.tsx— "Couldn't load the content model"cms/chat/error.tsx— "The chat agent failed to load"
Each is a 5-line client component rendering <AdminErrorView error={error} reset={reset} title="…" />.
AdminErrorView#
Located at octocms/admin/pages/AdminErrorView.tsx. Reads:
ContentSourceErrorinstance properties (code,userMessage) when the thrown value is a realContentSourceError.- The
CMS_PUBLIC:<code>:<message>serialized form (when the error has crossed a Server-Component boundary and lost its prototype).
Renders:
- A friendly heading (
titleprop, defaults to "Something went wrong"). - The parsed
userMessage, or a generic fallback. - A "try again in a few minutes" hint when
codeisgithub_unavailableorgithub_rate_limit. - A Try again button that calls
reset()(Next.js retries the failed segment). - A Dashboard link (or whatever
backHref/backLabelyou pass). - The error
digestreference (when present), formatted as a small uppercase footer.
Component-level <ErrorBoundary>#
octocms/components/ErrorBoundary/ErrorBoundary.tsx is a minimal class component (no extra dependency). API:
tsximport { ErrorBoundary } from 'octocms/components/ErrorBoundary/ErrorBoundary'; <ErrorBoundary label="markdown editor" // shown in the default fallback ("This {label} failed to load") fallback={<CustomFallback />} // optional: node or render fn ({ error, reset }) => ReactNode onReset={() => …} // optional: called from "Try again" + when resetKeys change resetKeys={[entry.id]} // optional: shallow-equality auto-reset trigger > <YourComponent /> </ErrorBoundary>
The default fallback is a small inline card with the heading, the error message, and a Try again button. Use a custom fallback when the surrounding chrome calls for something different (e.g. inside a list of chat messages).
Where it's wired#
| Component | Reason |
|---|---|
FormMarkdownField (MDXEditor) | Editor crashes shouldn't blank the entry editor. |
Composer (Chat) | Streaming + attachment bugs shouldn't kill the page. |
Message (Chat) | One bad message render shouldn't break the whole conversation. |
MediaManager (wrapped in MediaPage) | Drag-drop / upload paths involve a lot of stateful UI. |
ContentTypeDetail (wrapped in ContentTypePage) | Drag-reorder + dialogs. |
FormReferenceField (wrapped in FormFields) | Inline editor stack. |
If you find yourself adding a new high-risk client component (inline editors, drag-and-drop, dynamic imports, third-party widgets), wrap it in <ErrorBoundary> at the call site.
ContentSourceError codes#
Defined in octocms/lib/contentSourceError.ts:
| Code | When | UI copy hint |
|---|---|---|
github_config | Required env vars (GITHUB_REPO_OWNER, GITHUB_REPO_NAME, etc.) are missing. | "Set X in your environment." |
github_auth | 401 / 403 / 404 from the GitHub API — token missing or wrong permissions. | "Set CMS_GITHUB_TOKEN with Contents: Read." |
github_unavailable | 5xx, timeouts, network errors. | "Try again in a few minutes." |
github_rate_limit | 429. | "Wait a few minutes and try again." |
The mapGitHubApiErrorToContentSource(error, ctx) helper in the same file maps Octokit/REST errors into one of these codes. Use it whenever you wrap a GitHub call so the rest of the system can render consistent error copy.
Prod-fail-loud / dev-soft principle#
The public reads in octocms/github-public.ts (readGitHubFilePublic, listGitHubFiles, resolveContentBranch) follow this contract:
- In production, any
ContentSourceErrorfrom those helpers propagates upward and renders(public)/error.tsx. Empty / silent fallbacks are reserved for genuinely benign cases — file truly missing on a working token (returnsnull), directory truly empty (returns[]), invalid pointer JSON (falls back toconfigBranch). - In dev,
query()short-circuits to local FS viaoctocms/lib/localReader.tsbefore any GitHub helper is reached (gated onisProductionMode()inoctocms/query.ts). Dev therefore works offline; the GitHub helpers are not on the dev code path.
The asymmetry between the two helper modes:
| Status | readGitHubFilePublic | listGitHubFiles | resolveContentBranch |
|---|---|---|---|
| 200 | content / null (non-file) | string[] | parsed branch / configBranch |
| 401/403 | continue (try unauth) | continue (try unauth) | propagates from underlying read |
| 404, no auth errors yet | null (file missing) | [] (dir missing) | configBranch (pointer optional) |
| 404 after auth errors | continue / final throw github_auth | continue / final throw github_auth | propagates github_auth |
| 429 | throws github_rate_limit | throws github_rate_limit | propagates |
| 500, network | throws github_unavailable | throws github_unavailable | propagates |
Tests covering the matrix live in octocms/github-public.test.ts.
generateStaticParams and cacheComponents#
When cacheComponents is enabled (the default in this repo), generateStaticParams must return at least one entry. For dynamic routes that read GitHub-backed content (e.g. (public)/blog/[slug]):
- Real GitHub errors propagate and fail the build — that surfaces a missing/invalid
CMS_GITHUB_TOKENat deploy time instead of producing a silent empty deploy. - A legitimately empty collection (no posts yet) emits a single placeholder slug (
__placeholder__); the runtime page callsnotFound()so it serves a clean 404. This is purely a build-time shim, not an error-hiding mechanism.
Verifying#
Open /design#scr-error-states to see the route-level view rendered with each ContentSourceError code, plus a live <ErrorBoundary> you can toggle on and off.
To smoke-test in dev:
- Throw inside any admin page (
throw new Error('boom')in apage.tsx) — the matchingerror.tsxshould render. Click Try again to recover without a full reload. - Throw inside a wrapped client component (e.g.
Composer) — the inline<ErrorBoundary>fallback should show; the rest of the chat page stays interactive.