feat(ui): add React error boundary to Layout to recover from render errors#5539
feat(ui): add React error boundary to Layout to recover from render errors#5539q404365631 wants to merge 2 commits into
Conversation
✅ Deploy Preview for asyncapi-website ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughThis PR adds a React ErrorBoundary component and wraps Layout route render trees (docs, blog, posts, and default content) with it so render errors are caught, logged, and shown via a fallback UI instead of crashing the site. ChangesError boundary implementation and layout integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Tools execution failed with the following error: Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error) Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
⚡️ Lighthouse report for the changes in this PR:
Lighthouse ran on https://deploy-preview-5539--asyncapi-website.netlify.app/ |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/layout/Layout.tsx`:
- Around line 32-36: The ErrorBoundary currently wraps the entire
DocsLayout/BlogLayout/GenericPostLayout in components/layout/Layout.tsx so
layout chrome is replaced on child render errors and the boundary never resets;
change Layout.tsx to remove the outer ErrorBoundary and instead place
ErrorBoundary inside each layout component (DocsLayout, BlogLayout,
GenericPostLayout) so it only wraps the children prop, or add reset logic in
components/ErrorBoundary.tsx (reset hasError when the route/content key changes
via useRouter().asPath or an explicit key prop) so navigation clears the error
state.
- Around line 32-36: The ErrorBoundary instance in Layout.tsx never resets on
client navigation; wrap each ErrorBoundary in Layout.tsx (the ones around
DocsLayout, BlogLayout, and other branches) with a key driven by the router path
(e.g. import useRouter from 'next/router' and add key={router.asPath} to the
<ErrorBoundary> element) so a new boundary is mounted on navigation and has its
hasError state cleared; if you prefer, alternatively update
components/ErrorBoundary.tsx to accept a resetKey prop and clear hasError when
resetKey changes, but the quickest fix is adding key={router.asPath} to
ErrorBoundary in Layout.tsx for the affected blocks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 26d32a47-e34e-4401-800f-aae3fdab98f9
📒 Files selected for processing (2)
components/ErrorBoundary.tsxcomponents/layout/Layout.tsx
| <ErrorBoundary> | ||
| <DocsLayout post={post} navItems={allDocPosts}> | ||
| {children} | ||
| </DocsLayout> | ||
| </ErrorBoundary> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant layout file and the ErrorBoundary implementation.
ls -la
rg -n "function Layout|const Layout|export default function Layout|export default Layout" components/layout/Layout.tsx || true
sed -n '1,140p' components/layout/Layout.tsx
# Find ErrorBoundary definition and inspect its props/behavior.
rg -n "ErrorBoundary" components/layout/Layout.tsx components -S
# Attempt to locate the imported ErrorBoundary file.
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("components/layout/Layout.tsx")
txt = p.read_text()
m = re.search(r"from ['\"]([^'\"]*ErrorBoundary[^'\"]*)['\"]", txt)
print("import_guess:", m.group(1) if m else None)
PY
# Grep for the ErrorBoundary component definition.
fd -t f "ErrorBoundary.*" components pages src . 2>/dev/null || true
rg -n "export (default )?function ErrorBoundary|const ErrorBoundary|function ErrorBoundary|class ErrorBoundary" components -S || true
rg -n "resetKeys|fallbackRender|onReset|resetBoundary|useErrorBoundary|react-error-boundary" components -S || true
# Inspect the ErrorBoundary source file(s) if found.
python3 - <<'PY'
import subprocess, shlex, re, pathlib, os, json, sys
# Use rg results to pick likely files
res = subprocess.check_output(["bash","-lc","rg -l \"ErrorBoundary\" components | head -n 50"]).decode().splitlines()
print("\nCandidate files mentioning ErrorBoundary:")
for f in res[:30]:
print(f)
PYRepository: asyncapi/website
Length of output: 8841
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the full ErrorBoundary implementation (should be small).
sed -n '1,200p' components/ErrorBoundary.tsx
# Show Layout around the relevant branches with line numbers.
nl -ba components/layout/Layout.tsx | sed -n '1,140p'Repository: asyncapi/website
Length of output: 1945
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show pages/_app.tsx where Layout is mounted.
sed -n '1,200p' pages/_app.tsx
# Show Layout branches with line numbers (use awk instead of nl).
awk '{print NR ":" $0}' components/layout/Layout.tsx | sed -n '1,140p'
# Inspect layouts to see whether they render chrome around children or just children.
for f in components/layout/DocsLayout.tsx components/layout/BlogLayout.tsx components/layout/GenericPostLayout.tsx; do
echo "---- $f ----"
test -f "$f" && sed -n '1,220p' "$f" || echo "missing $f"
doneRepository: asyncapi/website
Length of output: 19431
Move ErrorBoundary to wrap children, not the whole docs/blog/post layout
In components/layout/Layout.tsx, ErrorBoundary wraps DocsLayout / BlogLayout / GenericPostLayout, so a render error in descendants replaces the entire section chrome (e.g., sidebar/TOC/header) with the fallback (see around lines 32–36, 45–49, 66–68). Also, components/ErrorBoundary.tsx has no reset logic—once hasError is set, it will keep showing the fallback until the boundary is unmounted/remounted (so in-branch navigation can get stuck).
Move the boundary inside each layout to wrap only the children slot (or add a reset mechanism keyed to the route/content).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/layout/Layout.tsx` around lines 32 - 36, The ErrorBoundary
currently wraps the entire DocsLayout/BlogLayout/GenericPostLayout in
components/layout/Layout.tsx so layout chrome is replaced on child render errors
and the boundary never resets; change Layout.tsx to remove the outer
ErrorBoundary and instead place ErrorBoundary inside each layout component
(DocsLayout, BlogLayout, GenericPostLayout) so it only wraps the children prop,
or add reset logic in components/ErrorBoundary.tsx (reset hasError when the
route/content key changes via useRouter().asPath or an explicit key prop) so
navigation clears the error state.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and print the relevant sections of Layout.tsx
FILE="components/layout/Layout.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "=== File: $FILE ==="
nl -ba "$FILE" | sed -n '1,140p'
# Find all ErrorBoundary usages in this file (and nearby contexts)
echo
echo "=== ErrorBoundary occurrences in $FILE ==="
rg -n "ErrorBoundary" "$FILE" || true
# Check where `pathname` comes from in this file (if referenced)
echo
echo "=== pathname references in $FILE ==="
rg -n "pathname" "$FILE" || true
# Identify ErrorBoundary import to see what props it supports (resetKey, etc.)
echo
echo "=== ErrorBoundary import lines in $FILE ==="
rg -n "import .*ErrorBoundary|from .*ErrorBoundary" "$FILE" || true
# If ErrorBoundary is a local component, find its definition
echo
echo "=== Searching for local ErrorBoundary component ==="
# Try common patterns
rg -n "function ErrorBoundary|const ErrorBoundary|class ErrorBoundary" components . || true
# If there's a file exporting ErrorBoundary, list it
echo
echo "=== Files exporting ErrorBoundary ==="
fd -t f "ErrorBoundary.*" . || trueRepository: asyncapi/website
Length of output: 165
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="components/layout/Layout.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "=== File: $FILE (first 160 lines) ==="
awk '{printf "%6d\t%s\n", NR, $0}' "$FILE" | sed -n '1,160p'
echo
echo "=== ErrorBoundary occurrences in $FILE ==="
rg -n "ErrorBoundary" "$FILE" || true
echo
echo "=== pathname references in $FILE ==="
rg -n "pathname" "$FILE" || true
echo
echo "=== ErrorBoundary import lines in $FILE ==="
rg -n "import .*ErrorBoundary|from .*ErrorBoundary" "$FILE" || true
echo
echo "=== Searching for local ErrorBoundary component definition (best-effort) ==="
rg -n "function ErrorBoundary|const ErrorBoundary|class ErrorBoundary" components . || true
echo
echo "=== Files exporting ErrorBoundary (best-effort) ==="
fd -t f -i "errorboundary" . || trueRepository: asyncapi/website
Length of output: 4259
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="components/ErrorBoundary.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "=== File: $FILE (first 220 lines) ==="
awk '{printf "%6d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'
echo
echo "=== ErrorBoundaryProps definition ==="
rg -n "interface ErrorBoundaryProps|type ErrorBoundaryProps|ErrorBoundaryProps" "$FILE" || true
echo
echo "=== reset-related props/methods ==="
rg -n "reset|componentDidUpdate|shouldComponentUpdate|getDerivedStateFromError|componentDidCatch|key" "$FILE" || trueRepository: asyncapi/website
Length of output: 2717
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find implementations
rg -n "function getDocBySlug|getDocBySlug\s*=\s*\(|getDocBySlug:" -S components/utils pages src utils . || true
rg -n "function getPostBySlug|getPostBySlug\s*=\s*\(|getPostBySlug:" -S components/utils pages src utils . || true
# Narrow to the utils/api import location used by Layout.tsx
# Layout imports from '../../utils/api' relative to components/layout -> utils/api.ts?(x)
fd -t f -a "api.*" utils components . 2>/dev/null || true
# Specifically print the api file(s) that contain getDocBySlug/getPostBySlug
fd -t f "api.*" . | head -n 50Repository: asyncapi/website
Length of output: 16700
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect utils/api.ts around getDocBySlug and getPostBySlug
FILE="utils/api.ts"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "=== File: $FILE (first 220 lines) ==="
awk '{printf "%6d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'
echo
echo "=== getDocBySlug implementation ==="
awk 'NR>=30 && NR<=90 {printf "%6d\t%s\n", NR, $0}' "$FILE"
echo
echo "=== getPostBySlug implementation ==="
awk 'NR>=1 && NR<=60 {printf "%6d\t%s\n", NR, $0}' "$FILE"
# Find where Layout.tsx is used (Next entrypoint)
echo
echo "=== Searching for Layout usage ==="
rg -n "from '.*components/layout/Layout'|components/layout/Layout|<Layout\b|function MyApp|_app\.tsx" . || true
# Prefer checking Next.js _app
echo
echo "=== Looking for pages/_app or app/layout ==="
rg -n "pages/_app|app/layout|_app\.tsx|appDir" . || true
# Print candidate entrypoints if found
echo
echo "=== Listing likely Next entry files ==="
fd -t f "_app\.(t|j)sx|_document\.(t|j)sx|layout\.(t|j)sx" pages app . || trueRepository: asyncapi/website
Length of output: 4609
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect _app.tsx
FILE="pages/_app.tsx"
echo "=== File: $FILE (first 140 lines) ==="
awk '{printf "%6d\t%s\n", NR, $0}' "$FILE" | sed -n '1,140p'
echo
echo "=== Listing pages/ route files related to docs/blog ==="
# list likely route files
fd -t f -a "pages" -x sh -lc 'echo {}' . 2>/dev/null | head -n 5 || true
# Use fd scoped to pages
fd -t f "docs" pages -a 2>/dev/null || true
fd -t f "blog" pages -a 2>/dev/null || true
# Also print all top-level pages files (limited)
echo
echo "=== Top-level pages/* (first 80 entries) ==="
find pages -maxdepth 2 -type f | sed -n '1,80p'Repository: asyncapi/website
Length of output: 2895
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search for docs routes in pages/ ==="
rg -n "docs" pages --glob='*.tsx' --glob='*.ts' || true
echo
echo "=== List likely docs page files ==="
fd -t f "docs" pages -d 4 || true
fd -t f "doc" pages -d 4 || true
echo
echo "=== Print pages under pages/** that include '/docs' in filename/path ==="
find pages -type f | rg -n "docs|doc" || true
echo
echo "=== Search for getStaticProps/getServerSideProps under docs pages ==="
# best-effort: look for getDocBySlug usage to find which pages call it
rg -n "getDocBySlug" pages components utils --glob='*.ts' --glob='*.tsx' || trueRepository: asyncapi/website
Length of output: 3473
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Count and list pages/ files (maxdepth 6, limited output) ==="
COUNT=$(find pages -type f | wc -l | tr -d ' ')
echo "pages files: $COUNT"
find pages -maxdepth 6 -type f | sort | sed -n '1,200p'
echo
echo "=== Find dynamic route patterns in pages/ ==="
rg -n "\[[^\]]+\]" pages --glob='*.tsx' --glob='*.ts' || true
echo
echo "=== Search for route files that mention '/docs' or 'docs' in their content ==="
rg -n "['\"]/docs['\"]|startsWith\\('/docs'|/docs/" pages --glob='*.tsx' --glob='*.ts' || true
echo
echo "=== Inspect config/posts.json for docs slug format ==="
POSTS_FILE="config/posts.json"
if [ ! -f "$POSTS_FILE" ]; then
POSTS_FILE="config/posts.json"
fi
if [ ! -f "$POSTS_FILE" ]; then
echo "Missing $POSTS_FILE" >&2
exit 1
fi
# Print first few occurrences of "/docs/" within the docs section for slug shape
# (Keep output small.)
rg -n "\"slug\"\s*:\s*\"/docs/" "$POSTS_FILE" --max-count 20 || true
# Also show a couple non-doc slugs to compare
rg -n "\"slug\"\s*:\s*\"/blog/" "$POSTS_FILE" --max-count 10 || true
echo
echo "=== Show a small sample around first /docs/ slug occurrence ==="
FIRST_LINE=$(rg -n "\"slug\"\s*:\s*\"/docs/" "$POSTS_FILE" | head -n 1 | cut -d: -f1 || true)
if [ -n "${FIRST_LINE:-}" ]; then
START=$((FIRST_LINE-5))
END=$((FIRST_LINE+20))
awk -v s=$START -v e=$END 'NR>=s && NR<=e {print NR ":" $0}' "$POSTS_FILE"
fiRepository: asyncapi/website
Length of output: 12231
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Find posts.json in repo ==="
fd -t f -i "posts.json" . || true
fd -t f "posts.json" . || true
echo
echo "=== Find config directory contents (if any) ==="
ls -la config 2>/dev/null || true
ls -la . | head
echo
echo "=== Inspect scripts/build-pages.ts ==="
FILE="scripts/build-pages.ts"
if [ -f "$FILE" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'
else
echo "Missing $FILE" >&2
fi
echo
echo "=== Inspect tests/build-pages.test.ts (if exists) ==="
FILE="tests/build-pages.test.ts"
if [ -f "$FILE" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'
else
echo "Missing $FILE" >&2
fi
echo
echo "=== Search for generated pages path patterns in build script output ==="
# look for "pages/" writes or templates
rg -n "pages/|mkdir|writeFile|fs\.|outputDir|outDir|docs|blog" scripts/build-pages.ts tests/build-pages.test.ts || trueRepository: asyncapi/website
Length of output: 12750
Reset the ErrorBoundary when navigation changes
components/ErrorBoundary.tsx sets hasError in state and never clears it, so once a boundary enters the fallback it will stay there. Since components/layout/Layout.tsx is mounted in pages/_app.tsx and the /docs/*//blog/* branches reuse the same (unkeyed) <ErrorBoundary> instance during client-side navigation, the fallback won’t clear when moving within the same branch. Key each <ErrorBoundary> by a navigation-changing value (use router.asPath), or add a resetKey-driven reset inside components/ErrorBoundary.tsx.
Apply to: components/layout/Layout.tsx line ranges 32-36, 45-49, 56-58, 66-68, and 74.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/layout/Layout.tsx` around lines 32 - 36, The ErrorBoundary
instance in Layout.tsx never resets on client navigation; wrap each
ErrorBoundary in Layout.tsx (the ones around DocsLayout, BlogLayout, and other
branches) with a key driven by the router path (e.g. import useRouter from
'next/router' and add key={router.asPath} to the <ErrorBoundary> element) so a
new boundary is mounted on navigation and has its hasError state cleared; if you
prefer, alternatively update components/ErrorBoundary.tsx to accept a resetKey
prop and clear hasError when resetKey changes, but the quickest fix is adding
key={router.asPath} to ErrorBoundary in Layout.tsx for the affected blocks.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #5539 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 23 23
Lines 931 931
Branches 180 180
=========================================
Hits 931 931 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|



What
Adds a top-level
ErrorBoundaryReact class component and wraps everyLayoutreturn tree in it, so a render error in any descendant (a broken MDX page, a future component, etc.) no longer takes the whole site down — it just shows a recoverable fallback.Why
As called out in #5047, the AsyncAPI website has zero React error boundaries. Today any uncaught render error in a layout child unmounts the entire route tree. Adding a top-level boundary is the standard, low-risk mitigation.
Changes
components/ErrorBoundary.tsx(66 lines): standard React error-boundary class component. Catches errors incomponentDidCatch, falls back to a small<div role="alert">(or a customfallbackprop), and logs toconsole.error. In production this is the natural seam to add a Sentry hook later.components/layout/Layout.tsx: ImportedErrorBoundaryand wrapped all 5 return branches (Docs container, Blogs container, Blogs sub-container, GenericPostLayout, default container) so a single error in any layout can't unmount the whole shell.2 files changed, 78 insertions(+), 9 deletions(-).
Closes #5047
Summary by CodeRabbit