🔥

[Next.js]Next.jsではtry-catchしない方が無難

2025/02/25に公開1

はじめに

ReactとNext.jsは、フレームワーク内部でthrowを活用しています。例えば、以下のようなケースが該当します。

  • Suspense は Promise を throw することで実現されている
  • RSCで使えるnotFound() はエラーを throw する関数である

そのため、安易にtry-catchを使うとフレームワークの機能を意図せずキャッチしてしまい、期待通りの動作にならないことがあります。

本記事では、Next.jsのRSC(React Server Components)でtry-catchを避けるべき理由と、その代替手段について解説します。

対象読者

  • それなりの規模で開発している開発者
  • Next.jsを利用している

try-catchが問題を引き起こすケース

SuspenseとPromiseのthrow

ReactのSuspenseは、Promiseをthrowすることでデータの読み込みを待機する仕組みを提供します。例えば、以下のようなコードを考えます。

async function fetchData() {
  return new Promise<string>((resolve) => {
    setTimeout(() => resolve("データ取得完了"), 1000);
  });
}
 
function SuspendedComponent() {
  throw fetchData(); // ここでPromiseをthrow
}
 
export default function Page() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <SuspendedComponent />
    </Suspense>
  );
}

このコードでは、Suspenseがthrow fetchData()をキャッチし、データが解決されるまでfallbackを表示します。

しかし、もしtry-catchでこれをキャッチしてしまうと、Suspenseの仕組みが機能しなくなり、意図しない動作につながります。

function SuspendedComponent() {
  try {
    throw fetchData();
  } catch (e) {
    console.error(e);
    return <p>エラーが発生しました</p>;
  }
}

この場合、fetchData()のPromiseがキャッチされてしまい、Suspenseの制御が失われます。
気を付けていてもライブラリが裏側でpromiseをthrowしている可能性もあります。try-catchしない方が無難です。

notFound()のthrow

Next.jsのnotFound()関数は、エラーをthrowすることで404ページへ遷移させる仕組みになっています。

import { notFound } from "next/navigation";

async function fetchPost(id: string) {
  const res = await fetch(`https://api.example.com/posts/${id}`);
  if (!res.ok) {
    notFound(); // ここでエラーをthrow
  }
  return res.json();
}

しかし、これをtry-catchでキャッチしてしまうと、ページ遷移が発生せず、不適切なエラーハンドリングになってしまいます。

async function fetchPost(id: string) {
  try {
    const res = await fetch(`https://api.example.com/posts/${id}`);
    if (!res.ok) {
      notFound(); // try-catchにより意図せずキャッチされる
    }
    return res.json();
  } catch (e) {
    console.error("エラーが発生しました", e);
    return null; // ここでnullを返してしまうと、意図しない挙動になる
  }
}

これも関数の奥でnotFound()を呼んでいる場合、気づくのが難しいので初めからtry-catchを避けた方が無難です。

try-catchの代替手段

エラーをreturnする

フレームワークのthrowを回避するためには、エラーをキャッチするのではなく、適切な戻り値として返す方法が考えられます。

type FetchResult<T> = { data: T | null; error?: Error };

async function fetchPost(id: string): Promise<FetchResult<Post>> {
  try {
    const res = await fetch(`https://api.example.com/posts/${id}`);
    if (!res.ok) {
      return { data: null, error: new Error("Not Found") };
    }
    return { data: await res.json() };
  } catch (e) {
    if (e instanceof Error) {
      return { data: null, error: e };
    }
    throw e;  // Error型じゃない場合はPromiseの場合があるのでre-throw
  }
}

このようにすることで、最低限のtry-catchのみでエラーハンドリングが可能になります。

Result型の導入

エラーを明示的に扱いたい場合は、Result型を導入するのも有効です。

type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

async function fetchPost(id: string): Promise<Result<Post, string>> {
  try {
    const res = await fetch(`https://api.example.com/posts/${id}`);
    if (!res.ok) {
      return { ok: false, error: new Error("Not Found") };
    }
    return { ok: true, value: await res.json() };
  } catch (e) {
    if (e instanceof Error) {
      return { ok: false, error: e };
    }
    throw e;  // Error型じゃない場合はPromiseの場合があるのでre-throw
}

呼び出し側では、okの値をチェックして適切に処理できます。

const result = await fetchPost("123");
if (!result.ok) {
  console.error(result.error);
} else {
  console.log(result.value);
}

まとめ

Next.jsのRSCでは、try-catchを安易に使うと、SuspenseやnotFound()のようなフレームワークのthrowを意図せずキャッチしてしまい、予期しない動作を引き起こす可能性があります。

そのため、

  • try-catchの使用を最小限に抑える
  • エラーはreturnで返すようにする
  • Result型を導入してエラーハンドリングを明示的にする

といった方針を取ることで、より安全なNext.jsアプリケーションを開発できます。
try-catchの範囲を最低限にとどめることで、フレームワークの機能を最大限に活用し、意図しないエラーを防ぎましょう。

Discussion

akfm_satoakfm_sato

notFound()などに関しては、unstable_rethrowというAPIを使えばtry catchしつつ機能を壊さないように実装することが可能です。

export default async function Page() {
  try {
    const post = await fetch('https://.../posts/1').then((res) => {
      if (res.status === 404) notFound()
      if (!res.ok) throw new Error(res.statusText)
      return res.json()
    })
  } catch (err) {
    unstable_rethrow(err) // `notFound()`の場合ここでre throw
    console.error(err)
  }
}

一応unstableですがシンプルなAPIですし、個人的には必要に応じてこのAPIで対処しようと考えてます。
ご存知でしたら失礼しました、ご参考までに。