この記事の対象読者
- fetchやaxiosを各画面で直接書いている人
- エラー処理の重複が気になってきた人
- APIレスポンスの型をそろえたい人
本題に入る前に
ある画面ではトースト、別の画面では alert、また別の画面では何も出ない。
APIエラー処理が画面ごとにバラバラになると、ユーザー体験も調査もしづらくなります。
最初は各画面で fetch を書くのが早いです。でも画面が増えると、毎回同じような try/catch、ステータス判定、エラーメッセージ変換が出てきます。
この記事では、TypeScriptで小さなAPIクライアントを作り、エラー処理をそろえる入口を紹介します。
エラーの種類を分ける
API呼び出しの失敗には、いくつか種類があります。
| 種類 | 例 |
|---|---|
| 業務エラー | 入力値が不正、権限がない |
| サーバーエラー | 500、外部API失敗 |
| ネットワークエラー | 通信できない、タイムアウト |
全部を同じ「失敗しました」にすると、画面側も調査側も困ります。
戻り値の型をそろえる
小さく ApiResult<T> を作ります。
type ApiSuccess<T> = {
ok: true;
data: T;
};
type ApiFailure = {
ok: false;
status?: number;
errorCode: string;
message: string;
};
type ApiResult<T> = ApiSuccess<T> | ApiFailure;
request 関数で、成功と失敗をこの形にそろえます。
async function request<T>(url: string, init?: RequestInit): Promise<ApiResult<T>> {
try {
const response = await fetch(url, init);
if (!response.ok) {
const body = await response.json().catch(() => null);
return {
ok: false,
status: response.status,
errorCode: body?.errorCode ?? "HTTP_ERROR",
message: body?.message ?? "処理に失敗しました",
};
}
const data = (await response.json()) as T;
return { ok: true, data };
} catch {
return {
ok: false,
errorCode: "NETWORK_ERROR",
message: "通信に失敗しました。時間をおいて再度お試しください。",
};
}
}
画面側は表示に集中する
画面では、戻り値の形がそろっているので扱いやすくなります。
type Reservation = {
id: string;
title: string;
};
async function loadReservation() {
const result = await request<Reservation>("/api/reservations/1");
if (!result.ok) {
return {
errorMessage: result.message,
};
}
return {
reservation: result.data,
};
}
画面ごとにHTTPステータスやJSONパースを毎回書かなくてよくなります。
ログに残したい情報もそろえる
APIクライアントでエラーを受けたとき、必要ならログ向け情報もそろえます。
function toClientLog(error: ApiFailure) {
return {
operation: "api_request",
status: "failure",
httpStatus: error.status,
errorCode: error.errorCode,
};
}
画面表示とログ情報を分けると、ユーザーに見せすぎず、調査には必要な情報を残せます。
まとめ
APIエラー処理は、画面ごとに書いていると少しずつズレます。
小さなAPIクライアントを作り、成功と失敗の戻り値をそろえる。画面側は、その結果をどう表示するかに集中する。
これだけでも、フロントエンドのエラー処理はかなり読みやすくなります。