neverthrow と fetch と zod を組み合わせた非同期処理
こんにちは。フロントエンドエンジニアの辻です。
Gaji-Labo ではデザイナーとエンジニアが協力して、「手ざわりのいいUI」の実現に向けて、日々励んでいます。
フロントエンドエンジニアとしては、デザイン通りに実装するのはもちろんの事、UI の裏側で動作するロジックにも同じように注力しています。今回はその裏側のロジックについての話です。
はじめに、フロントエンドにおけるロジック実装の一手として neverthrow というライブラリを紹介します。
次に neverthrow と fetch と zod を組み合わせた非同期処理を書いていきます。
neverthrow とは?
neverthrow とは、JavaScript(TypeScript)においてエラーをスローせずに Result 型として扱うライブラリです。その名の通りですね。
neverthrow を導入すると、JavaScript(TypeScript)の try ~ catch 構文を、Rust 言語における Result 型に似た機能で代用できるため、より安全で堅牢なコードを記述できます。
> neverthrow 公式ページ
> Result – Rust By Example
以下は neverthrow のサンプルコードです。
import { Result, ok, err } from "neverthrow";
/**
 * 引数の target を numbers 配列のいずれかの値で除算する関数です。
 * もし 0 で除算しようとした場合は err() を返します。
 * 0 以外で除算する場合は ok() を返します。
 */
const divide = (target: number): Result<number, string> => {
  const numbers = [0, 1, 2];
  const index = Math.floor(Math.random() * numbers.length);
  if (numbers[index] === 0) return err("0で割ることはできません。");
  return ok(target / numbers[index]);
};
/**
 * isOk() は処理が成功したか(okを返したか)を判定します。
 */
const result = divide(10);
if (result.isOk()) {
  console.log(result.value); // 10 or 5
} else {
  console.log(result.error); // 0で割ることはできません。
}neverthrow と非同期処理を組み合わせてみる
とても便利な neverthrow ですが、私は非同期処理と組み合わせて使っています。
非同期処理はそれ自体の難易度もさることながら、エラーハンドリングも考慮すると一筋縄ではいきません。
そこで neverthrow の出番です。
さっそくサンプルコードを用意しました。
import { Result, ok, err } from "neverthrow";
/**
 * neverthrow と fetch を組み合わせた関数です。
 * ジェネリクスの R は API のレスポンスの型。P はリクエストパラメータの型です。
 */
export const handleFetch = <R, P = unknown>({
  path,
  method = "GET",
  requestParams,
}: {
  path: string;
  method?: "GET" | "POST" | "PUT" | "DELETE";
  requestParams?: P;
}): Promise<Result<R, Error>> => {
  // リクエストパラメータの整備
  const params: RequestInit = {
    method,
  };
  if ((method === "POST" || method === "PUT") && requestParams) {
    params.body = JSON.stringify(requestParams);
  }
  // fetch の実行
  return fetch(path, params)
    .then((res) => {
      if (!res.ok) {
        // 通信に失敗した場合は例外を投げます
        // 最終的に、例外は catch 句にて err() でラップして返します
        throw new Error("サーバーとの通信に失敗しました");
      }
      return res.json();
    })
    .then((data: R) => {
      return ok(data);
    })
    .catch((error: Error) => {
      return err(error);
    });
};今回用意した handleFetch 関数ですが、リクエストパラメータの整備や fetch の実行については、一般的な非同期処理と変わりありません。
neverthrow を導入した事による変化として、戻り値が ok() か err() でラップされている点が挙げられます。
これにより、handleFetch 関数の戻り値の型は Promise<Result<R, Error>> となり、handleFetch 関数の呼び出し元では isOk() や isErr() が扱えます。
実際に使ってみるとこんな感じです。
import { handleFetch } from "./neverthrow_fetch";
type Todo = {
  id: number;
  userId: number;
  title: string;
  completed: boolean;
};
const run = async () => {
  const result = await handleFetch<Todo>({
    path: "https://jsonplaceholder.typicode.com/todos/1",
  });
  if (result.isOk()) {
    console.log(result.value); // { "userId": 1, "id": 1, "title": "title text", "completed": false }
  } else {
    console.log(result.error);
  }
};
run();ちなみに、neverthrow は非同期の Result 型を表現する機能として ResultAsync を用意しています。
今回は分かりやすさを優先して Promise<Result<T, E>> としていますが、開発時は ResultAsync<T, E>とした方がより効率的でしょう。
> Asynchronous API (ResultAsync) | neverthrow
zod と組み合わせてみる
最後に handleFetch 関数と zod を組み合わせて使ってみます。
zod はスキーマのバリデーションライブラリです。シンプルな使い勝手ながらも機能は強力です。
先程の handleFetch 関数と zod を組み合わせたサンプルコードです。
import { err, ok, fromPromise } 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) => {
      // URL 末尾に入る id を zod で検証して、その結果を ok() か err() に振り分ける … (1)
      if (!id.success) return err(id.error);
      return ok(id.data);
    })
    .asyncAndThen((id) => {
      // 検証後の id を利用して API を実行する … (2)
      // fromPromise は Promise<Result<Todo, Error>> を ResultAsync として扱うため
      return fromPromise(
        handleFetch<Todo>({
          path: `https://jsonplaceholder.typicode.com/todos/${id}`,
        }),
        (error) => error
      );
    })
    .map((result) => {
      // 一連の処理が成功した場合 … (3)
      if (result.isOk()) {
        console.log(result.value);
      }
    })
    .mapErr((error) => {
      // エラーが発生した場合 … (4)
      if (error instanceof ZodError) {
        console.log("zod エラー", error);
      } else {
        console.log("API エラー", error);
      }
    });
};
run(1);zod の safeParse 関数の結果を、ok() に入れる所からはじめて、
- URL 末尾に入る id を zod で検証して、その結果を 
ok()かerr()に振り分ける - zod の検証が成功後に、その結果を利用して API を実行する
 - 一連の処理が成功した場合、その結果を出力する
 - エラーが発生した場合、 
instanceof構文でエラーの種別を判定して、エラーハンドリングを行う 
…の流れになっています。
今回利用した neverthrow の andThen、asyncAndThen、map、mapErr の機能は、それぞれ次の通りです。
andThen と asyncAndThen
andThen 関数は、Result 型オブジェクトに対して連続した操作をするために利用します。イメージとしては非同期処理における then が近いですね。
andThen 関数は、Result 型オブジェクトが Ok の場合にのみ、引数の関数を実行します。もし Result 型オブジェクトが Err であれば引数の関数は実行されず、そのまま Err を返します。
先程のサンプルコードでは、zod で検証した結果を ok() か err() に振り分けています。
また、andThen 関数が Result 型オブジェクトを扱うのに対して、asyncAndThen 関数は ResultAsync 型オブジェクトを扱います。
handleFetch 関数の実行時に asyncAndThen 関数を利用しているのは、そのためです。
> Result.asyncAndThen | neverthrow
map
map 関数も andThen 関数と似たような動きをします。
map 関数も Result 型オブジェクトが Ok の場合に、引数の関数を実行します。もし Result 型オブジェクトが Err であれば引数の関数は実行されず、そのまま Err を返します。
andThen 関数がより複雑な処理に向いているのに対して、map 関数は Ok の値を変換する処理に向いています。(今回はただログを出力しているだけですが…)
andThen も asyncAndThen も map も、Result 型オブジェクトが Ok の場合のみ、引数の関数を発火させます。途中で処理が失敗して Err になっていた場合は、引数に指定した関数は発火しません。
先程のサンプルコードでは、run 関数の引数に 1 〜 100 以外の数値を入れると Zod エラーが発生するため、後続の handleFetch 関数が発火しないようになっています。
mapErr
map 関数と対となるのが mapErr 関数です。
mapErr 関数は Result 型オブジェクトが Err の場合に引数の関数を実行します。
先程のサンプルコードでは、Zod エラーと API エラーの 2 つをまとめて mapErr 関数で処理しています。
まとめ
簡単なサンプルではありますが、neverthrow と fetch の組み合わせから、zod との併用までを見てきました。
非同期処理に neverthrow を導入すれば、パラメータ検証 → API 実行 → レスポンスの加工 + エラーハンドリングを一連の処理として記述できます。
非同期処理で try ~ catch 構文を利用すると、どうしても複雑なコードになってしまいますし、エラーが発生した場合に処理フローを追いかけるだけでも一苦労です。
そんな時は、ぜひ neverthrow を検討してみてはいかがでしょうか?
続編となる記事「neverthrow と fetch と zod を組み合わせた非同期処理【直列実行と並列実行編】」を執筆しました。良ければコチラもご覧ください。
Gaji-Labo では「手ざわりのよいUI」の実現を、デザイナーとエンジニアが一緒になって目指しています。
カジュアル面談の場も用意しておりますので、「UI コンポーネントの設計・実装が得意!」という方も「業務ロジックの実装が得意!」という方も、ぜひお気軽にご連絡ください!
Gaji-Labo フロントエンドエンジニア向けご案内資料
Gaji-Labo は Next.js, React, TypeScript 開発の実績と知見があります
フロントエンド開発の専門家である私たちが御社の開発チームに入ることで、バックエンドも含めた全体の開発効率が上がります。
「既存のサイトを Next.js に移行したい」
「人手が足りず信頼できるエンジニアを探している」
「自分たちで手を付けてみたがいまいち上手くいかない」
フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にご相談ください。
オンラインでのヒアリングとフルリモートでのプロセス支援にも対応しています。
Next.js, React, TypeScript の相談をする!





