iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
Modern Web

從 React 學 Next.js:不只要會用,還要真的懂系列 第 19

【Day 19】React vs Next.js 設定客製化錯誤頁面

  • 分享至 

  • xImage
  •  

今天我們接著來看另一個當我們在初期設置專案時,會特別去設定的的部分,那就是「客製化錯誤頁面」。 我們一樣會先看 React 的部分,再看回來 Next.js 的部分。

React 的客製化錯誤頁面方式

在 React 中,當我們在使用 config 檔設定 Router 時,還會去設定一個也很重要的部分,那就是當進入沒有定義在 config 檔案中的網址時的錯誤頁面。雖然說沒有特別去做設定,也會有顯示一個找不到此網頁的畫面,但是為了讓整體的樣式一致,通常還是會去做這部分的客製化設定。

在 React 中定義 Router 時,加上下述這個部分,就可以設定的 404 Not found 的頁面,透過這個方式可以自定義這個頁面的樣式。

const router = createBrowserRouter([
  // 略
  {
    path: '*',
    element: <NotFound />,
  },
]);

除了找不到頁面的處理,還有一個情境是其他錯誤的頁面(例如 runtime 的 render 錯誤),想要設定這樣的錯誤頁面,可以在 React 定義 Router 的 config 檔案中透過 errorElement 把錯誤內容帶上。

const router = createBrowserRouter([
  {
    path: '/',
    element: <HomePage />,
    errorElement: <ErrorPage />, // 當該路由出現錯誤時,會顯示這個頁面
    children: [
      {
        path: 'about',
        element: <AboutPage />,
      },
    ],
  },
]);

在 Next.js 設定客製化的錯誤頁面

在 Next.js 的 Page Router 和 App Router 模式下一樣也可以針對前面提到過的那兩個情境做頁面的設定,主要是透過設定指的檔案名稱來客製化相對應的錯誤頁面。

Page Router 使用 404、500 檔案和 _error 檔案

在 Page Router 中,要客製化錯誤頁面的方式很簡單,只要在 Pages 資料夾底下建立 404、500 檔案及 _error 檔案就可以了。

客製化 404、500 錯誤出現時的錯誤頁面

如果想要針對 404 和 500 這兩個錯誤自定義一個顯示的頁面,可以直接在 Pages 資料夾底下用 404 和 500 作為檔案名稱建立檔案(如下圖那樣設定檔案),檔案內的寫法就依照寫普通元件的方式寫就可以。
https://ithelp.ithome.com.tw/upload/images/20250913/20130914q0aL23yfHk.png

const custom404Page = () => {
  return (
    <div>
      <h1>404 - Page Not Found!!!</h1>
    </div>
  );
};

export default custom404Page;

不過如果是 API 請求發生錯誤(例如 HTTP status 500),Next.js 並不會自動顯示 500 或 _error 頁面。必須手動 throw new Error()來觸發對應的錯誤頁面。

這樣設定之後,當進入一個未定義的路由時,就會出現這個客製化的 404 頁面。
https://ithelp.ithome.com.tw/upload/images/20250913/201309146S76EJ2q6x.png

以此類推,當碰到 500 的錯誤時,也會依照設定的 500 檔案內容去顯示在頁面上。

客製化全域性的錯誤頁面

有別於 404 和 500 檔案,_error 是一個全域性的錯誤檔案,可以作為 fallback 的錯誤頁面來使用,如果沒有設定 404 或 500 檔案時,會使用 _error 檔案來顯示錯誤頁面,當遇到出現其他 HTTP 的錯誤(例如: 401、403、502 等),也會用 _error 這個檔案的內容來顯示。在全域的錯誤頁面一樣也會需要手動去 throw new Error(),以觸發顯示客製化的全域錯誤頁面。因此在 Page Router 中,若希望顯示客製化的錯誤頁面,不僅要設定好 _error 檔案,還需要在程式邏輯中主動丟出錯誤來觸發對應的錯誤顯示。

最後需要強調的是「在 Page Router 中的客製化定義的頁面都是全域套用的錯誤頁面,無法以巢狀的方法設定特定路由中要顯示另一種錯誤頁面」。

App Router 定義客製化錯誤頁面的方式

在 App Router 中,和 Page Router 一樣,也是透過建立特定名稱的檔案來定義客製化的錯誤頁面,不過使用的檔案名稱會有些差異,而且有別於 Page Router,在 App Router 中有支援巢狀的錯誤頁面設定。

使用 not-found 檔案處理 404 錯誤

有別於 Page Router 的用法,在 App Router 中,如果要客製化 404 錯誤的頁面時,要建立的是名為 not-found 的檔案。

如果這樣做的話,當進入一個不存在的路由,就會顯示對應的錯誤頁面。
https://ithelp.ithome.com.tw/upload/images/20250913/20130914H2OVICiCGz.png

const CustomNotFoundPage = () => {
  return <p className="text-bold text-4xl">custom not found Page</p>;
};

export default CustomNotFoundPage;

例如,我們並沒有在 Router 中設定 /detail 這個 Route,當輸入這個沒有設定的 Route http://localhost:3000/detail,就會會出現如下這樣的頁面。
https://ithelp.ithome.com.tw/upload/images/20250913/20130914ZgUKJBZnwD.png

這邊再嘗試一個寫法,那就是在巢狀結構中,設定另一個 not-found 檔案。
https://ithelp.ithome.com.tw/upload/images/20250913/20130914Zkn2AAdBmh.png

如果想要在巢狀結構中,讓應巢狀結構的 not-found 頁面能夠套用對應的 layout 設定,單純只是在對應的資料夾底下加上 not-found 檔案是不夠的,會需要透過呼叫 notFound() 來手動觸發
這裡我們會需要調整一下 [id] 資料夾底下的 page 檔案內容,把一些判斷加上去。這裡假設的是 id 不可能等於 1,所以會在 page 中寫下相關判斷,並且執行 notFound()。

import Link from "next/link";
import { notFound } from "next/navigation";

const AboutIdPage = async ({ params }) => {
  const { id } = await params;

  // 假設 id 永遠不可能等於 1,所以不可能會有 about/1 這頁
  if (id === "1") {
    notFound();
  }

  return (
    <div>
      <p>About B</p>
      <Link className="text-blue-500 underline" href="/">
        Home
      </Link>
    </div>
  );
};

export default AboutIdPage;

這時候進入 about/1 的路由就會出現以下截圖的畫面,這樣也就能讓這個錯誤頁面套用對應路由段(特定資料夾底下)中設定的 layout,所以在這個情境中,不只有出現 logo header,還有出現 sidebar menu。
https://ithelp.ithome.com.tw/upload/images/20250913/20130914RSVrfAtOL6.png

fallback 用的 error 頁面

在 Next.js App Router 中,若要針對非 404 的錯誤頁面進行客製化,可以使用名為 error 的檔案。但是 error 檔案只能捕捉 render 階段的錯誤,對於 HTTP 錯誤(例如:fetch 回傳 500)並不會自動觸發。如果想要在這個情況下,顯示客製化的 error 頁面,會需要手動 throw new Error()。除此之外,還需要注意的是 error 檔案一定要是 Client Component,所以一定要加上 "use client"

error 檔案的內容可以這樣設定。

"use client"; // Error boundaries must be Client Components

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {

  return (
    <div>
      <h2>Something went wrong!! other error!!</h2>
      <button className="w-fit p-2 bg-amber-500" onClick={() => reset()}>
        Try again
      </button>
    </div>
  );
}

設定完後,當出現非 404 的錯誤時,就會出現 error 檔案設定的畫面。
https://ithelp.ithome.com.tw/upload/images/20250913/20130914WV9NgvBs9T.png

error 檔案的使用一樣也可以設定在巢狀結構中,當有 404 以外的錯誤出現時,也就會在套用相對應的 layout 下,顯示錯誤頁面。
https://ithelp.ithome.com.tw/upload/images/20250913/20130914d8wR4TW0Yd.png

套用在全域上的 global-error

前面已經提到 not-found 檔案是用來定義 404 error 出現的頁面,error 檔案則是用來定義除了 404 以外的 error 的頁面,那 global-error 呢?global-error 與前面提到的用來客製化的錯誤檔案不同,是全域性的錯誤檔案,他啟動時間點不是在進入特定 Router 的 layout 後,global-error 檔案會在「任何 layout 尚未成功初始化時」就捕捉錯誤。例如當 layout 本身的程式碼出錯,就會進入這個錯誤頁面。也是因為這樣的緣故,global-error 這個錯誤頁面不會套用到任何路由的 layout,且只能設定在 app 資料夾的最底層,不會套用巢狀路由的設定。

當有錯誤出現,也就會是一個沒有套用到我們 layout 檔案所設定的 header 的錯誤頁面。
https://ithelp.ithome.com.tw/upload/images/20250913/20130914aUICHFU45p.png

App Router 中錯誤處理的最佳實踐

前面已經看了 Page Router 和 App Router 關於設定客製化錯誤頁面的方式了,現在再來看看關於 Next.js 官方建議的錯誤處理方式。

需要處理的錯誤主要可以分為兩大類:
∙ 可預期的錯誤:像是送出資料時的參數沒帶齊、權限不足等可以預期的情況所導致的錯誤。
∙ 不可預期的錯誤:DB 端出現問題、後端 API 沒有正確部署上去等前端無法預期到會發生的狀況。

針對這兩種錯誤情境,建議將可預期的錯誤直接透過畫面 UI 來提示(例如:toast 或畫面上顯示紅字提示錯誤),不另外使用整頁的錯誤頁來處理,不可預期的錯誤才使用整頁的 error 頁面來處理。會建議這麼做的原因是如果連可預期的錯誤(例如:表單填寫資料有誤),都以整頁的錯誤來提示的話,會影響到整體使用流程,造成使用者體驗不佳,所以只有不可預期的錯誤才使用整頁的錯誤頁面來處理會比較合適。

總結

在 React 時期,可以在 React Router 中,用 path="*" 設定客製化的 404 Not Found,也能在各路由設定 errorElement 來處理其他的錯誤。到了 Next.js,Page Router 提供全域共用的 404、500、 _error 頁面;而 App Router 則可針對每個每個路由段 (Route Segment) 提供各自的 error 與 not-found 頁面檔案,並自動套用對應的巢狀 layout。因此能依不同路徑與錯誤狀態呈現對應的客製化頁面,同時維持版面一致性,整體彈性與可維護性更高。

到這裡為止就是客製化錯誤頁面的部分。明天一樣會接著來看設定特定路由可進入的權限,阻擋特定角色無法進入特定頁面並且進行頁面導轉的部分,那就明天見囉!。

參考資料

官方文件 - Custom Errors
官方文件 - error handling


上一篇
【Day 18】React vs Next.js 內部連結的使用方式 - <Link>
系列文
從 React 學 Next.js:不只要會用,還要真的懂19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言