Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions packages/solid-router/src/Match.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,11 @@ export const Outlet = () => {
const shouldShowNotFound = () =>
childMatchStatus() !== 'redirected' && parentGlobalNotFound()

const rootErrorComponent = () =>
router.routesById[rootRouteId]?.options.errorComponent ??
router.options.defaultErrorComponent
const rootResetKey = useRouterState({ select: (s) => s.loadedAt })
Comment on lines +391 to +394
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mirror the full root Match boundary behavior here.

This restores the root errorComponent, but it still diverges from the normal root path at Lines 102-143: onCatch no longer forwards to the root/default onCatch, and isNotFound(error) is rethrown without a sibling root CatchNotFound. In the direct-<Outlet /> shell case, ordinary errors will skip root onCatch, and a descendant errorComponent that escalates with throw notFound(...) can still bypass the root notFoundComponent.

Proposed parity fix
   const rootErrorComponent = () =>
     router.routesById[rootRouteId]?.options.errorComponent ??
     router.options.defaultErrorComponent
+  const rootOnCatch = () =>
+    router.routesById[rootRouteId]?.options.onCatch ??
+    router.options.defaultOnCatch
+  const rootNotFoundComponent = () =>
+    router.routesById[rootRouteId]?.options.notFoundComponent ??
+    router.options.notFoundRoute?.options.component
   const rootResetKey = useRouterState({ select: (s) => s.loadedAt })

   return (
@@
             <Solid.Suspense
               fallback={
                 <Dynamic component={router.options.defaultPendingComponent} />
               }
             >
-              {rootErrorComponent() ? (
-                <CatchBoundary
-                  getResetKey={() => rootResetKey()}
-                  errorComponent={rootErrorComponent()!}
-                  onCatch={(error) => {
-                    if (isNotFound(error)) throw error
-                  }}
-                >
-                  {childMatch()}
-                </CatchBoundary>
-              ) : (
-                childMatch()
-              )}
+              <Dynamic
+                component={rootErrorComponent() ? CatchBoundary : SafeFragment}
+                getResetKey={() => rootResetKey()}
+                errorComponent={rootErrorComponent() || ErrorComponent}
+                onCatch={(error: Error) => {
+                  if (isNotFound(error)) throw error
+                  warning(false, `Error in route match: ${rootRouteId}`)
+                  rootOnCatch()?.(error)
+                }}
+              >
+                <Dynamic
+                  component={
+                    rootNotFoundComponent() ? CatchNotFound : SafeFragment
+                  }
+                  fallback={(error: any) => {
+                    if (
+                      !rootNotFoundComponent() ||
+                      (error.routeId && error.routeId !== rootRouteId)
+                    ) {
+                      throw error
+                    }
+
+                    return (
+                      <Dynamic
+                        component={rootNotFoundComponent()}
+                        {...error}
+                      />
+                    )
+                  }}
+                >
+                  {childMatch()}
+                </Dynamic>
+              </Dynamic>
             </Solid.Suspense>

Also applies to: 420-432

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/solid-router/src/Match.tsx` around lines 391 - 394, The root Match
boundary changes must mirror the full root behavior: update the Match component
logic that defines rootErrorComponent and rootResetKey so its onCatch handler
forwards to the root/default onCatch (call or delegate to router.options.onCatch
or the root route's onCatch) instead of swallowing errors, ensure
isNotFound(error) results in rethrow only when a sibling root CatchNotFound is
present otherwise forward to the root notFound handler
(router.options.notFoundComponent or route-level notFoundComponent), and in the
direct-Outlet shell path ensure ordinary errors still bubble to the root onCatch
and descendant errorComponents that throw notFound(...) are captured by a
sibling root CatchNotFound equivalent; locate and modify the onCatch/code paths
and the error escalation flow near rootErrorComponent/rootResetKey and the
Outlet-shell branch to delegate to the router's root/default handlers.


return (
<Solid.Show
when={!shouldShowNotFound() && childMatchId()}
Expand All @@ -398,20 +403,33 @@ export const Outlet = () => {
}
>
{(matchIdAccessor) => {
// Use a memo to avoid stale accessor errors while keeping reactivity
const currentMatchId = Solid.createMemo(() => matchIdAccessor())

const childMatch = () => <Match matchId={currentMatchId()} />

return (
<Solid.Show
when={routeId() === rootRouteId}
fallback={<Match matchId={currentMatchId()} />}
fallback={childMatch()}
>
<Solid.Suspense
fallback={
<Dynamic component={router.options.defaultPendingComponent} />
}
>
<Match matchId={currentMatchId()} />
{rootErrorComponent() ? (
<CatchBoundary
getResetKey={() => rootResetKey()}
errorComponent={rootErrorComponent()!}
onCatch={(error) => {
if (isNotFound(error)) throw error
}}
>
{childMatch()}
</CatchBoundary>
) : (
childMatch()
)}
</Solid.Suspense>
</Solid.Show>
)
Expand Down