Skip to content

Commit 445a7b8

Browse files
committed
Move catchError to next/error
1 parent 7afafdb commit 445a7b8

File tree

21 files changed

+503
-69
lines changed

21 files changed

+503
-69
lines changed

crates/next-core/src/next_import_map.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ pub async fn get_next_edge_import_map(
456456
rcstr!("next/app") => rcstr!("next/dist/api/app"),
457457
rcstr!("next/document") => rcstr!("next/dist/api/document"),
458458
rcstr!("next/dynamic") => rcstr!("next/dist/api/dynamic"),
459+
rcstr!("next/error") => rcstr!("next/dist/api/error"),
459460
rcstr!("next/form") => rcstr!("next/dist/api/form"),
460461
rcstr!("next/head") => rcstr!("next/dist/api/head"),
461462
rcstr!("next/headers") => rcstr!("next/dist/api/headers"),

crates/next-custom-transforms/src/transforms/react_server_components.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,19 @@ use regex::Regex;
1111
use rustc_hash::FxHashMap;
1212
use serde::Deserialize;
1313
use swc_core::{
14-
atoms::{atom, Atom, Wtf8Atom},
14+
atoms::{Atom, Wtf8Atom, atom},
1515
common::{
16+
DUMMY_SP, FileName, Span, Spanned,
1617
comments::{Comment, CommentKind, Comments},
1718
errors::HANDLER,
1819
util::take::Take,
19-
FileName, Span, Spanned, DUMMY_SP,
2020
},
2121
ecma::{
2222
ast::*,
23-
utils::{prepend_stmts, quote_ident, quote_str, ExprFactory},
23+
utils::{ExprFactory, prepend_stmts, quote_ident, quote_str},
2424
visit::{
25-
noop_visit_mut_type, noop_visit_type, visit_mut_pass, Visit, VisitMut, VisitMutWith,
26-
VisitWith,
25+
Visit, VisitMut, VisitMutWith, VisitWith, noop_visit_mut_type, noop_visit_type,
26+
visit_mut_pass,
2727
},
2828
},
2929
};
@@ -656,6 +656,7 @@ impl ReactServerComponentValidator {
656656
"useFormState",
657657
],
658658
),
659+
(atom!("next/error").into(), vec!["catchError"]),
659660
(
660661
atom!("next/navigation").into(),
661662
vec![

packages/next/error.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import Error from './dist/pages/_error'
2-
export * from './dist/pages/_error'
1+
import Error from './dist/api/error'
2+
export * from './dist/api/error'
33
export default Error

packages/next/error.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,12 @@
1-
module.exports = require('./dist/pages/_error')
1+
let errorModule
2+
3+
try {
4+
errorModule = require('./dist/api/error')
5+
} catch {
6+
// In react-server-conditioned environments (e.g. instrumentation/proxy),
7+
// evaluating pages/_error can throw before compile errors are surfaced.
8+
// Fallback to an empty module so import diagnostics can still be reported.
9+
errorModule = {}
10+
}
11+
12+
module.exports = errorModule

packages/next/errors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1052,5 +1052,6 @@
10521052
"1051": "Turbopack trace server is not supported on this platform (%s/%s) because native bindings are not available. Only WebAssembly (WASM) bindings were loaded, and Turbopack requires native bindings.",
10531053
"1052": "Turbopack is not supported on this platform (%s/%s) because native bindings are not available. Only WebAssembly (WASM) bindings were loaded, and Turbopack requires native bindings. Use the --webpack flag instead.",
10541054
"1053": "Export encountered errors on %s %s:\\n\\t%s",
1055-
"1054": "`catchError` can only be used in Client Components. Add the \"use client\" directive to use it."
1055+
"1054": "`catchError` can only be used in Client Components. Add the \"use client\" directive to use it.",
1056+
"1055": "`retry()` can only be used in the App Router. Use `reset()` in the Pages Router."
10561057
}

packages/next/src/api/error.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// App Router
2+
export { catchError } from '../client/components/catch-error'
3+
export type { ErrorInfo } from '../client/components/error-boundary'
4+
5+
// Pages Router
6+
export { default } from '../pages/_error'
7+
export * from '../pages/_error'

packages/next/src/build/create-compiler-aliases.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ export function createServerOnlyClientOnlyAliases(
202202

203203
export function createNextApiEsmAliases() {
204204
const mapping = {
205+
error: 'next/dist/api/error',
205206
head: 'next/dist/api/head',
206207
image: 'next/dist/api/image',
207208
constants: 'next/dist/api/constants',

packages/next/src/client/components/catch-error-boundary.tsx

Lines changed: 0 additions & 23 deletions
This file was deleted.

packages/next/src/client/components/catch-error.tsx

Lines changed: 194 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,209 @@
1-
import React, { type JSX } from 'react'
2-
import { CatchErrorBoundary } from './catch-error-boundary'
1+
'use client'
2+
3+
import React, { startTransition, type JSX } from 'react'
4+
import { HandleISRError } from './handle-isr-error'
5+
import { handleHardNavError } from './nav-failure-handler'
6+
import { useUntrackedPathname } from './navigation-untracked'
7+
import { unstable_rethrow } from './unstable-rethrow'
8+
import { isBot } from '../../shared/lib/router/utils/is-bot'
9+
import {
10+
AppRouterContext,
11+
type AppRouterInstance,
12+
} from '../../shared/lib/app-router-context.shared-runtime'
13+
import { RouterContext } from '../../shared/lib/router-context.shared-runtime'
314
import type { ErrorInfo } from './error-boundary'
415

16+
const isBotUserAgent =
17+
typeof window !== 'undefined' && isBot(window.navigator.userAgent)
18+
519
export type FallbackComponent<P> = (
620
props: P & { children?: React.ReactNode },
721
errorInfo: ErrorInfo
822
) => React.ReactNode
923

24+
type CatchErrorWrapperProps<P> = P & { children?: React.ReactNode }
25+
type NextErrorBoundaryProps<P> = CatchErrorWrapperProps<P> & {
26+
pathname: string | null
27+
isPagesRouter: boolean
28+
}
29+
30+
type NextErrorBoundaryState = {
31+
error: Error | null
32+
previousPathname: string | null
33+
componentStack: React.ErrorInfo['componentStack']
34+
ownerStack: ReturnType<typeof React.captureOwnerStack>
35+
}
36+
37+
type FallbackWrapperProps<P> = {
38+
props: P
39+
errorInfo: ErrorInfo
40+
}
41+
42+
function createFallbackWrapper<P extends Record<string, any>>(
43+
fallback: FallbackComponent<P>
44+
): React.ComponentType<FallbackWrapperProps<P>> {
45+
const fallbackName = fallback.name || 'Unknown'
46+
const fallbackWrapper = {
47+
[fallbackName]: ({ props, errorInfo }: FallbackWrapperProps<P>) =>
48+
fallback(props, errorInfo),
49+
}[fallbackName] as React.ComponentType<FallbackWrapperProps<P>>
50+
51+
if (process.env.NODE_ENV !== 'production') {
52+
fallbackWrapper.displayName = fallbackName
53+
}
54+
55+
return fallbackWrapper
56+
}
57+
1058
export function catchError<P extends Record<string, any>>(
1159
fallback: FallbackComponent<P>
1260
): React.ComponentType<P & { children?: React.ReactNode }> {
13-
function CatchErrorWrapper(
14-
props: P & { children?: React.ReactNode }
15-
): JSX.Element {
16-
const { children, ...componentProps } = props
61+
const FallbackWrapper = createFallbackWrapper(fallback)
62+
63+
class NextErrorBoundary extends React.Component<
64+
NextErrorBoundaryProps<P>,
65+
NextErrorBoundaryState
66+
> {
67+
static contextType = AppRouterContext
68+
declare context: AppRouterInstance | null
69+
70+
constructor(props: NextErrorBoundaryProps<P>) {
71+
super(props)
72+
this.state = {
73+
error: null,
74+
previousPathname: this.props.pathname,
75+
componentStack: undefined,
76+
ownerStack: null,
77+
}
78+
}
79+
80+
static getDerivedStateFromError(error: Error): {
81+
error: Error
82+
ownerStack: ReturnType<typeof React.captureOwnerStack>
83+
} {
84+
unstable_rethrow(error)
85+
86+
let ownerStack: string | null = null
87+
if ('captureOwnerStack' in React) {
88+
ownerStack = React.captureOwnerStack()
89+
}
90+
91+
return { error, ownerStack }
92+
}
93+
94+
componentDidCatch(_error: Error, errorInfo: React.ErrorInfo): void {
95+
this.setState({
96+
componentStack: errorInfo.componentStack,
97+
})
98+
}
99+
100+
static getDerivedStateFromProps(
101+
props: NextErrorBoundaryProps<P>,
102+
state: NextErrorBoundaryState
103+
): NextErrorBoundaryState | null {
104+
const { error } = state
105+
106+
// If we encounter an error while a navigation is pending, don't render
107+
// the fallback and let the hard navigation attempt to recover instead.
108+
if (process.env.__NEXT_APP_NAV_FAIL_HANDLING) {
109+
if (error && handleHardNavError(error)) {
110+
return {
111+
error: null,
112+
previousPathname: props.pathname,
113+
componentStack: undefined,
114+
ownerStack: null,
115+
}
116+
}
117+
}
118+
119+
// Reset error state when navigation changes pathname so boundaries don't
120+
// stay latched while navigating to a different route.
121+
if (props.pathname !== state.previousPathname && error) {
122+
return {
123+
error: null,
124+
previousPathname: props.pathname,
125+
componentStack: undefined,
126+
ownerStack: null,
127+
}
128+
}
129+
130+
return {
131+
error: state.error,
132+
previousPathname: props.pathname,
133+
componentStack: state.componentStack,
134+
ownerStack: state.ownerStack,
135+
}
136+
}
137+
138+
clearError = () => {
139+
this.setState({
140+
error: null,
141+
componentStack: undefined,
142+
ownerStack: null,
143+
})
144+
}
145+
146+
reset = () => {
147+
this.clearError()
148+
}
149+
150+
retry = () => {
151+
const router = this.context
152+
if (this.props.isPagesRouter || router === null) {
153+
throw new Error(
154+
'`retry()` can only be used in the App Router. Use `reset()` in the Pages Router.'
155+
)
156+
}
157+
158+
startTransition(() => {
159+
router.refresh()
160+
this.clearError()
161+
})
162+
}
163+
164+
render(): React.ReactNode {
165+
if (this.state.error && !isBotUserAgent) {
166+
const {
167+
children: _children,
168+
pathname: _pathname,
169+
...componentProps
170+
} = this.props
171+
const errorInfo: ErrorInfo = {
172+
error: this.state.error,
173+
reset: this.reset,
174+
retry: this.retry,
175+
componentStack: this.state.componentStack,
176+
ownerStack: this.state.ownerStack,
177+
}
178+
179+
return (
180+
<>
181+
<HandleISRError error={this.state.error} />
182+
<FallbackWrapper
183+
props={componentProps as unknown as P}
184+
errorInfo={errorInfo}
185+
/>
186+
</>
187+
)
188+
}
189+
190+
return this.props.children
191+
}
192+
}
193+
194+
if (process.env.NODE_ENV !== 'production') {
195+
;(NextErrorBoundary as any).displayName = 'Next.ErrorBoundary'
196+
}
17197

198+
function CatchErrorWrapper(props: CatchErrorWrapperProps<P>): JSX.Element {
199+
const pathname = useUntrackedPathname()
200+
const pagesRouter = React.useContext(RouterContext)
18201
return (
19-
<CatchErrorBoundary
20-
fallback={fallback}
21-
componentProps={componentProps as P}
22-
>
23-
{children}
24-
</CatchErrorBoundary>
202+
<NextErrorBoundary
203+
{...props}
204+
pathname={pathname}
205+
isPagesRouter={pagesRouter !== null}
206+
/>
25207
)
26208
}
27209

packages/next/src/client/components/navigation.react-server.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export { notFound } from './not-found'
1111
export { forbidden } from './forbidden'
1212
export { unauthorized } from './unauthorized'
1313
export { unstable_rethrow } from './unstable-rethrow'
14-
export { catchError } from './catch-error'
1514
export { ReadonlyURLSearchParams }
1615

1716
export const RedirectType = {

0 commit comments

Comments
 (0)