Show navigationHide navigation

Error handling

OctoCMS layers error coverage in two places:

  1. Route-level error.tsx — one per admin segment, rendering a shared AdminErrorView. Catches anything thrown during render of that segment's page.tsx.
  2. 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 fallback
  • cms/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:

  • ContentSourceError instance properties (code, userMessage) when the thrown value is a real ContentSourceError.
  • The CMS_PUBLIC:<code>:<message> serialized form (when the error has crossed a Server-Component boundary and lost its prototype).

Renders:

  • A friendly heading (title prop, defaults to "Something went wrong").
  • The parsed userMessage, or a generic fallback.
  • A "try again in a few minutes" hint when code is github_unavailable or github_rate_limit.
  • A Try again button that calls reset() (Next.js retries the failed segment).
  • A Dashboard link (or whatever backHref / backLabel you pass).
  • The error digest reference (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:

tsx
import { 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#

ComponentReason
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:

CodeWhenUI copy hint
github_configRequired env vars (GITHUB_REPO_OWNER, GITHUB_REPO_NAME, etc.) are missing."Set X in your environment."
github_auth401 / 403 / 404 from the GitHub API — token missing or wrong permissions."Set CMS_GITHUB_TOKEN with Contents: Read."
github_unavailable5xx, timeouts, network errors."Try again in a few minutes."
github_rate_limit429."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 ContentSourceError from 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 (returns null), directory truly empty (returns []), invalid pointer JSON (falls back to configBranch).
  • In dev, query() short-circuits to local FS via octocms/lib/localReader.ts before any GitHub helper is reached (gated on isProductionMode() in octocms/query.ts). Dev therefore works offline; the GitHub helpers are not on the dev code path.

The asymmetry between the two helper modes:

StatusreadGitHubFilePubliclistGitHubFilesresolveContentBranch
200content / null (non-file)string[]parsed branch / configBranch
401/403continue (try unauth)continue (try unauth)propagates from underlying read
404, no auth errors yetnull (file missing)[] (dir missing)configBranch (pointer optional)
404 after auth errorscontinue / final throw github_authcontinue / final throw github_authpropagates github_auth
429throws github_rate_limitthrows github_rate_limitpropagates
500, networkthrows github_unavailablethrows github_unavailablepropagates

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_TOKEN at 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 calls notFound() 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:

  1. Throw inside any admin page (throw new Error('boom') in a page.tsx) — the matching error.tsx should render. Click Try again to recover without a full reload.
  2. Throw inside a wrapped client component (e.g. Composer) — the inline <ErrorBoundary> fallback should show; the rest of the chat page stays interactive.