错误处理
错误分两类:
- 可预期错误:表单校验失败、请求失败 — 把它们当作「返回值」,不要
throw。 - 未捕获异常:真正的 bug — 抛出去,由错误边界兜底。
处理可预期错误
Server Function
校验失败、请求非 2xx,应该返回错误对象:
'use server';
export async function createPost(prevState: any, formData: FormData) {
const res = await fetch('https://api.vercel.app/posts', {
method: 'POST',
body: JSON.stringify({
title: formData.get('title'),
content: formData.get('content'),
}),
});
if (!res.ok) return { message: '创建失败' };
return { message: '' };
}
客户端用 useActionState 读取:
'use client';
import { useActionState } from 'react';
import { createPost } from '@/app/actions';
export function Form() {
const [state, formAction, pending] = useActionState(createPost, { message: '' });
return (
<form action={formAction}>
<input name="title" required />
<textarea name="content" required />
{state?.message && <p aria-live="polite">{state.message}</p>}
<button disabled={pending}>提交</button>
</form>
);
}
在服务端组件中处理
直接根据响应决定渲染分支或重定向:
export default async function Page() {
const res = await fetch('https://…');
if (!res.ok) return <p>出错了</p>;
const data = await res.json();
return <Posts data={data} />;
}
404:notFound
在路由段里调用 notFound() 就会渲染最近的 not-found.tsx:
import { notFound } from 'next/navigation';
import { getPostBySlug } from '@/lib/posts';
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) notFound();
return <div>{post.title}</div>;
}
配套的 app/blog/not-found.tsx:
export default function NotFound() {
return <div>404 - 找不到页面</div>;
}
处理未捕获异常
嵌套错误边界
在任意路由段放 error.tsx 就会为该子树建立错误边界。必须加 'use client':
'use client';
import { useEffect } from 'react';
export default function ErrorPage({
error,
unstable_retry,
}: {
error: Error & { digest?: string };
unstable_retry: () => void;
}) {
useEffect(() => console.error(error), [error]);
return (
<div>
<h2>出错了</h2>
<button onClick={() => unstable_retry()}>重试</button>
</div>
);
}
错误会冒泡到最近的 error.tsx,因此你可以分层定制。例如 /dashboard/settings/error.tsx 会兜底设置页,其它页面则走上层 error.tsx。
更细粒度:unstable_catchError
想把错误边界挂到组件级别,可以用 unstable_catchError:
'use client';
import { unstable_catchError as catchError, type ErrorInfo } from 'next/error';
function Fallback(
props: { title: string },
{ error, unstable_retry }: ErrorInfo
) {
return (
<div>
<h2>{props.title}</h2>
<p>{error.message}</p>
<button onClick={() => unstable_retry()}>重试</button>
</div>
);
}
export default catchError(Fallback);
事件里的错误
错误边界只捕获渲染期错误。事件处理函数、async 回调里的错误要自己 try/catch 并用 useState 存起来:
'use client';
import { useState } from 'react';
export function Button() {
const [error, setError] = useState<any>(null);
function handleClick() {
try {
throw new Error('Oops');
} catch (e) {
setError(e);
}
}
if (error) return <p>发生错误:{error.message}</p>;
return <button onClick={handleClick}>点我</button>;
}
注意:startTransition 里的错误会 冒泡到最近的错误边界。
全局兜底:global-error.js
根布局崩了怎么办?在 app/global-error.tsx 提供最外层兜底,它会替换根布局:
'use client';
export default function GlobalError({
error,
unstable_retry,
}: {
error: Error & { digest?: string };
unstable_retry: () => void;
}) {
return (
<html>
<body>
<h2>出错了</h2>
<button onClick={() => unstable_retry()}>重试</button>
</body>
</html>
);
}
注意 global-error 自己必须写 <html> 和 <body>。