第6部:品質と効率化 Step 17 / 24

エラーハンドリング

エラーは必ず起きるもの。適切に処理してユーザー体験を損なわないようにしましょう。

FastAPIのエラー処理

HTTPException

from fastapi import FastAPI, HTTPException, status

app = FastAPI()

@app.get("/posts/{post_id}")
def get_post(post_id: int):
    post = db.query(Post).filter(Post.id == post_id).first()

    if not post:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Post not found"
        )

    return post


@app.post("/posts")
def create_post(post: PostCreate, current_user: User = Depends(get_current_user)):
    if not current_user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Not authenticated",
            headers={"WWW-Authenticate": "Bearer"}
        )

    # 作成処理...
    return new_post

カスタム例外ハンドラー

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

# カスタム例外
class PostNotFoundError(Exception):
    def __init__(self, post_id: int):
        self.post_id = post_id

# 例外ハンドラー登録
@app.exception_handler(PostNotFoundError)
async def post_not_found_handler(request: Request, exc: PostNotFoundError):
    return JSONResponse(
        status_code=404,
        content={
            "error": "PostNotFound",
            "message": f"Post with id {exc.post_id} not found",
        }
    )

# バリデーションエラーのカスタマイズ
from fastapi.exceptions import RequestValidationError

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=422,
        content={
            "error": "ValidationError",
            "details": exc.errors()
        }
    )

Next.jsのエラー処理

error.tsx(エラー境界)

"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="text-center">
        <h2 className="text-2xl font-bold text-red-600 mb-4">
          問題が発生しました
        </h2>
        <p className="text-gray-600 mb-6">
          {error.message || "予期しないエラーが発生しました"}
        </p>
        <button
          onClick={reset}
          className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
        >
          もう一度試す
        </button>
      </div>
    </div>
  );
}

not-found.tsx(404ページ)

import Link from "next/link";

export default function NotFound() {
  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="text-center">
        <h1 className="text-6xl font-bold text-gray-300 mb-4">404</h1>
        <h2 className="text-2xl font-bold mb-4">ページが見つかりません</h2>
        <p className="text-gray-600 mb-6">
          お探しのページは存在しないか、移動した可能性があります。
        </p>
        <Link
          href="/"
          className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
        >
          トップページへ
        </Link>
      </div>
    </div>
  );
}

notFound()の呼び出し

import { notFound } from "next/navigation";

async function getPost(id: string) {
  const res = await fetch(`${API_URL}/posts/${id}`);

  if (res.status === 404) {
    notFound();  // not-found.tsx を表示
  }

  if (!res.ok) {
    throw new Error("Failed to fetch post");  // error.tsx を表示
  }

  return res.json();
}

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await getPost(params.id);

  return <div>{post.title}</div>;
}

API呼び出しのエラー処理

"use client";

import { useState } from "react";

export function CreatePostForm() {
  const [error, setError] = useState<string | null>(null);
  const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError(null);
    setFieldErrors({});

    try {
      const res = await fetch("/api/posts", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(formData),
      });

      if (!res.ok) {
        const data = await res.json();

        // バリデーションエラー
        if (res.status === 422) {
          const errors: Record<string, string> = {};
          data.details?.forEach((err: any) => {
            const field = err.loc[err.loc.length - 1];
            errors[field] = err.msg;
          });
          setFieldErrors(errors);
          return;
        }

        // 認証エラー
        if (res.status === 401) {
          setError("ログインが必要です");
          return;
        }

        // その他のエラー
        setError(data.message || "エラーが発生しました");
        return;
      }

      // 成功
      alert("投稿しました!");

    } catch (err) {
      // ネットワークエラーなど
      setError("通信エラーが発生しました。インターネット接続を確認してください。");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {error && (
        <div className="bg-red-100 text-red-700 p-3 rounded mb-4">
          {error}
        </div>
      )}

      <div>
        <input name="title" />
        {fieldErrors.title && (
          <p className="text-red-500 text-sm">{fieldErrors.title}</p>
        )}
      </div>

      <button type="submit">投稿</button>
    </form>
  );
}

ユーザーフレンドリーなエラー表示

悪い例

  • ✗ "Error: ECONNREFUSED"
  • ✗ "500 Internal Server Error"
  • ✗ "undefined is not a function"

技術的な内容は見せない

良い例

  • ○ "通信エラーが発生しました"
  • ○ "入力内容に誤りがあります"
  • ○ "しばらく経ってから再度お試しください"

次のアクションを示す

まとめ

  • FastAPIはHTTPExceptionで明示的にエラーを返す
  • Next.jsはerror.tsx, not-found.tsxでエラー画面を設定
  • API呼び出しはtry-catchで囲む
  • ユーザーには分かりやすいメッセージを表示
データフェッチ 次へ:テスト入門