neverthrow と fetch と zod を組み合わせた非同期処理【タイムアウト編】
こんにちは。フロントエンドエンジニアの辻です。
Gaji-Labo ではデザイナーとエンジニアが協力して、「手ざわりのいいUI」の実現に向けて、日々励んでいます。
UIの見た目の実装はもちろん大切ですが、UIの裏側で動作するロジックの実装も同じように大切です。
今回も「手ざわりのいいUI」を縁の下から支えるロジック面に関わる内容です。前回に引き続き neverthrow と非同期処理について見ていきます!
さて、前回の記事「neverthrow と fetch と zod を組み合わせた非同期処理【直列実行と並列実行編】」では、neverthrow と fetch と zod を組み合わせて、直列実行と並列実行を試してみました。
今回は、前回に改修した handleFetch 関数にタイムアウトを組み込んでみます。
fetch のタイムアウト
現代フロントエンドにおける fetch のタイムアウト処理は、かなり簡単に実装できるようになりました。AbortSignal.timeout
関数を fetch の signal
パラメータに渡すだけで、タイムアウトを設定できます。
fetch("https://jsonplaceholder.typicode.com/todos/", {
signal: AbortSignal.timeout(5 * 1000), // 5秒でタイムアウトとする
})
.then((result) => result.json())
.then((data) => { console.log(data) })
.catch((error) => {
if (error.name === "TimeoutError") {
// タイムアウトした場合の処理
console.error("Timeout Error");
} else {
console.error("Other Error");
}
});
> AbortSignal: timeout() 静的メソッド | MDN Web Docs
タイムアウトを考慮した neverthrow + fetch のサンプルコード
それでは、タイムアウトを考慮して handleFetch 関数を改修してみましょう。
前回のコードから、下記のように改修しました。
import { ResultAsync } from "neverthrow";
/**
* neverthrow の ResultAsync と fetch を組み合わせた関数です。
*/
export const handleFetch = <R, P = unknown>({
path,
method = "GET",
requestParams,
timeoutMilliSecond = 30 * 1000, // タイムアウトの時間を追加。デフォルトで 30 秒 …(1)
}: {
path: string;
method?: "GET" | "POST" | "PUT" | "DELETE";
requestParams?: P;
timeoutMilliSecond?: number;
}): ResultAsync<R, Error> => {
const params: RequestInit = {
method,
signal: AbortSignal.timeout(timeoutMilliSecond), // signal を追加 …(2)
};
if ((method === "POST" || method === "PUT") && requestParams) {
params.body = JSON.stringify(requestParams);
}
return ResultAsync.fromPromise(
fetch(`${path}`, params).then((response) => {
if (!response.ok) {
throw new Error("サーバーとの通信に失敗しました");
}
return response.json().then((data: R) => {
const isR = isTypeR<R>(data);
if (!isR) {
throw new Error("レスポンスの形式が不正です");
}
return data;
});
}),
(error) => {
if (error instanceof Error) {
if (error.message === "サーバーとの通信に失敗しました") {}
if (error.message === "レスポンスの形式が不正です") {}
if (error.name === "TimeoutError") {
// タイムアウト時の処理をココに記述する …(3)
return new Error("サーバーが所定時間内に応答しませんでした");
}
return error;
}
return new Error("不明なエラーが発生しました");
}
);
};
/**
* fetch の戻り値を推論するための型ガード
*/
const isTypeR = <R>(data: unknown): data is R => {
return typeof data === "object" && data !== null;
};
改修点は、至ってシンプルです。
まず、タイムアウトの時間 timeoutMilliSecond
を新たに引数に設定します。今回はデフォルトで 30 秒としています。(1)
次に、timeoutMilliSecond
を fetch のパラメータのsignal
に渡します。(2)
これで fetch にタイムアウトを組み込めました。
最後にタイムアウト時のエラーハンドリングを実装します。
…と言っても、今回はただ単純に error.name === "TimeoutError"
の条件分岐を追加して、新たにエラーオブジェクトを投げているだけですね。(3)
では、改修した新 handleFetch 関数を呼び出してみましょう。
import { err, ok } from "neverthrow";
import { z, ZodError } from "zod";
import { handleFetch } from "./neverthrow_fetch";
type Todo = {
id: number;
userId: number;
title: string;
completed: boolean;
};
const run = (id: number) => {
ok(z.number().min(1).max(100).safeParse(id))
.andThen((id) => {
if (!id.success) return err(id.error);
return ok(id.data);
})
.asyncAndThen((id) => {
return handleFetch<Todo>({
path: `https://jsonplaceholder.typicode.com/todos/${id}`,
timeoutMilliSecond: 10 * 1000, // タイムアウトの時間を明示的に設定
});
})
.map((result) => {
console.log(result);
})
.mapErr((error) => {
if (error instanceof ZodError) {
console.log("zod エラー", error);
} else {
// タイムアウトが発生した場合は「サーバーが所定時間内に応答しませんでした」が出力される
console.log("API エラー", error);
}
});
};
run(1)
handleFetch 関数の呼び出し側から今回の改修点を見てみると、ただ timeoutMilliSecond
というオプションパラメータが追加されただけですので、今までと使い勝手は変わりません。
唯一異なる点として mapErr
のエラーハンドリング関数にて、タイムアウト時のエラーを扱えるようになった点が挙げられます。error.message
が「サーバーが所定時間内に応答しませんでした」であれば、タイムアウトが発生したと判断できますね。
(サンプルコードのため、エラー判定が粗雑です。本格的に実装する場合は、きちんとエラーオブジェクトを定義すると良いでしょう。)
まとめ
非同期処理に neverthrow を導入すれば、パラメータ検証 → API 実行 → レスポンスの加工 + エラーハンドリング(タイムアウト等のエラーでもなんでも対応可)を1つにまとめられるため、可読性・メンテナンス性が向上します!
また、直列実行であっても並列実行であっても柔軟に対応できます。何でもござれですね!
Gaji-Labo では「手ざわりのよいUI」の実現を、デザイナーとエンジニアが一緒になって目指しています。
カジュアル面談の場も用意しておりますので、「UI コンポーネントの設計・実装が得意!」という方も「業務ロジックの実装が得意!」という方も、ぜひお気軽にご連絡ください!
Gaji-Labo フロントエンドエンジニア向けご案内資料
Gaji-Labo は Next.js, React, TypeScript 開発の実績と知見があります
フロントエンド開発の専門家である私たちが御社の開発チームに入ることで、バックエンドも含めた全体の開発効率が上がります。
「既存のサイトを Next.js に移行したい」
「人手が足りず信頼できるエンジニアを探している」
「自分たちで手を付けてみたがいまいち上手くいかない」
フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にご相談ください。
オンラインでのヒアリングとフルリモートでのプロセス支援にも対応しています。
Next.js, React, TypeScript の相談をする!