🤔

【JS/TS】カスタムエラーを作成する

に公開

Javascriptにはエラークラス(Error)がありエラー時にはこれをスローすることがあるかなと思います。

throw new Error(`エラーが発生しました。ステータス=${res.status},メッセージ=${res.message}`)

スローすることでプログラムがエラーで止まってしまいます。
プログラムを止める仕様なら良いですが、プログラムを止めずになんらかの表示を変えたい時は

return new Error(`エラーが発生しました。ステータス=${res.status},メッセージ=${res.message}`)

とすると思います。

メッセージ以外に材料を追加したいという人もいるでしょう。
確かに要素は追加はできます。

ですがデメリットがあります。

  • 全て一律Error
    • 分類がしにくい
  • 型安全性に欠ける
  • IDEの補完で扱いにくい
  • 頑張らないとチーム開発の際の一貫性が出てこなくなる

カスタムをしたいときは下記のようにするのではないでしょうか?

export class CustomError extends Error {
    message:string;
    hogehoge:string;

    constructor(message:string,hogehoge:string){
        //変数に代入〜
    }
}

とはいえ、私はクラスを使っていません。
neverthrowなどといったものとの親和性がものすごく高くパターンマッチも型安全にできるからといった理由を持っています。。

実際にカスタムエラーを作っていこうと思います。

カスタムエラーの作成(HttpError)

今回はHttpErrorを例に作成していきます。

カスタムエラーのコアの型定義

interfaceを使って型を作っていきます。

//objectErrorは今回は出てきません。
export type ErrorType = "httpError" | "fetchError" | "objectError";

export interface CustomError {
    type: ErrorType;
    message: string;
}

スキームが少ない気がしますが一旦このくらいで良いでしょう。
今後ツールを作っていきながら増やしていこうというのんびりスタイルでやってます(仕事以外は)。

httpエラーのスキーム作り

では保守しやすいように、データスキームをまとめておこうと思います。

export interface HttpCustomStatusScheme {
    returnNotFoundAPIUrl: 4041;
    returnNoPermission: 4031;
    returnBadRequest: 4001;
    returnInternalServerError: 5001;
}
export interface HttpErrorStatusResponse {
    notFound: 404;
    internalServerError: 500;
    forbidden: 403;
    badRequest: 400;
}

export interface HttpErrorStatusErrorMessage {
    returnNotFoundAPIUrl: string;
    returnNoPermission: string;
    returnBadRequest: string;
    returnInternalServerError: string;
}

export type HttpStatus =
    | HttpErrorStatusResponse["notFound"]
    | HttpErrorStatusResponse["internalServerError"]
    | HttpErrorStatusResponse["forbidden"]
    | HttpErrorStatusResponse["badRequest"];

export type HttpCustomStatus =
    | HttpCustomStatusScheme["returnNotFoundAPIUrl"] //httpからのレスポンスで404が返ってきた
    | HttpCustomStatusScheme["returnNoPermission"] //権限がない
    | HttpCustomStatusScheme["returnBadRequest"] //不正なリクエスト
    | HttpCustomStatusScheme["returnInternalServerError"]; // サーバーエラー

export interface HttpErrorScheme {
    httpErrorStatusResponse: HttpErrorStatusResponse;
    httpCustomStatusScheme: HttpCustomStatusScheme;
    httpErrorMessage: HttpErrorStatusErrorMessage;
}

export const createHttpScheme: HttpErrorScheme = (function () {
    /**API仕様で変更 */
    const httpErrorStatusResponse: HttpErrorStatusResponse = {
        notFound: 404,
        internalServerError: 500,
        forbidden: 403,
        badRequest: 400
    };
    /**API仕様で変更 */
    const httpCustomStatusScheme: HttpCustomStatusScheme = {
        returnNotFoundAPIUrl: 4041,
        returnNoPermission: 4031,
        returnBadRequest: 4001,
        returnInternalServerError: 5001
    };

    /**API仕様や画面仕様で変更 */
    const httpErrorMessage: HttpErrorStatusErrorMessage = {
        returnNotFoundAPIUrl: "APIのURLが見つかりません",
        returnNoPermission: "権限がありません",
        returnBadRequest: "不正なリクエストです",
        returnInternalServerError: "サーバーエラーです"
    };

    return {
        httpErrorStatusResponse: httpErrorStatusResponse,
        httpCustomStatusScheme: httpCustomStatusScheme,
        httpErrorMessage: httpErrorMessage
    };
})();

この辺は人の好みとかプロジェクトによりますがこのようにまとめておけばステータス等を追加しやすいですし、編集もしやすいでしょう。

ちなみにテストコードは以下の通りです。

import { createHttpScheme } from "@/utils/error/http/http-scheme";
import { describe, expect, it } from "vitest";

describe("http-scheme", () => {
    it("createHttpSchemeにおいてhttpErrorStatusResponseの正常な値が返ってくる", () => {
        const httpErrorScheme = createHttpScheme;

        expect(httpErrorScheme.httpErrorStatusResponse.notFound).toEqual(404);
        expect(
            httpErrorScheme.httpErrorStatusResponse.internalServerError
        ).toEqual(500);
        expect(httpErrorScheme.httpErrorStatusResponse.badRequest).toEqual(400);
        expect(httpErrorScheme.httpErrorStatusResponse.forbidden).toEqual(403);
    });

    it("createHttpSchemeにおいてhttpCustomStatusScheme", () => {
        const httpErrorScheme = createHttpScheme;

        expect(
            httpErrorScheme.httpCustomStatusScheme.returnNotFoundAPIUrl
        ).toEqual(4041);
        expect(
            httpErrorScheme.httpCustomStatusScheme.returnNoPermission
        ).toEqual(4031);
        expect(httpErrorScheme.httpCustomStatusScheme.returnBadRequest).toEqual(
            4001
        );
    });

    it("createHttpSchemeにおいてerrorMessage", () => {
        const httpErrorScheme = createHttpScheme;

        expect(httpErrorScheme.httpErrorMessage.returnNotFoundAPIUrl).toEqual(
            "APIのURLが見つかりません"
        );
        expect(httpErrorScheme.httpErrorMessage.returnNoPermission).toEqual(
            "権限がありません"
        );
        expect(httpErrorScheme.httpErrorMessage.returnBadRequest).toEqual(
            "不正なリクエストです"
        );

        expect(
            httpErrorScheme.httpErrorMessage.returnInternalServerError
        ).toEqual("サーバーエラーです");
    });
});

HttpErrorの作成

HttpErrorを作成していきます。
ベースは

  1. 型定義
  2. スキーム定義
  3. 各エラーのスキーム作成

型定義

まずは型定義です。

export interface HttpError extends CustomError {
    status: HttpCustomStatus;
}

エラースキームを作成する関数

const createHttpError = ({
    status,
    message
}: {
    status: HttpCustomStatus;
    message: string;
}): HttpError => {
    return {
        type: "httpError",
        status,
        message
    };
};

この関数を作っておけば、後々楽になるでしょう。

各ステータス返却関数

const returnNotFoundAPIUrl = (function () {
    return createHttpError({
        status: httpErrorScheme.httpCustomStatusScheme.returnNotFoundAPIUrl,
        message: httpErrorScheme.httpErrorMessage.returnNotFoundAPIUrl
    });
})();

const returnNoPermission = (function () {
    return createHttpError({
        status: httpErrorScheme.httpCustomStatusScheme.returnNoPermission,
        message: httpErrorScheme.httpErrorMessage.returnNoPermission
    });
})();

const returnBadRequest = (function () {
    return createHttpError({
        status: httpErrorScheme.httpCustomStatusScheme.returnBadRequest,
        message: httpErrorScheme.httpErrorMessage.returnBadRequest
    });
})();

const returnInternalServerError = (function () {
    return createHttpError({
        status: httpErrorScheme.httpCustomStatusScheme
                .returnInternalServerError,
        message: httpErrorScheme.httpErrorMessage.returnInternalServerError
    });
})();

今回は即時関数じゃなくてただのオブジェクトでも良いのかなとは思いましたが、処理等が入る場合もありそうなので即時関数として表現しておきます。

全体像

import {
    createHttpScheme,
    HttpCustomStatus
} from "@/utils/error/http/http-scheme";
import { CustomError } from "../core/core-error";

export interface HttpError extends CustomError {
    status: HttpCustomStatus;
}

/**ここは仕様に応じて変更する*/
export const createHttpError = (function () {
    const httpErrorScheme = createHttpScheme;

    const createHttpError = ({
        status,
        message
    }: {
        status: HttpCustomStatus;
        message: string;
    }): HttpError => {
        return {
            type: "httpError",
            status,
            message
        };
    };

    const returnNotFoundAPIUrl = (function () {
        return createHttpError({
            status: httpErrorScheme.httpCustomStatusScheme.returnNotFoundAPIUrl,
            message: httpErrorScheme.httpErrorMessage.returnNotFoundAPIUrl
        });
    })();

    const returnNoPermission = (function () {
        return createHttpError({
            status: httpErrorScheme.httpCustomStatusScheme.returnNoPermission,
            message: httpErrorScheme.httpErrorMessage.returnNoPermission
        });
    })();

    const returnBadRequest = (function () {
        return createHttpError({
            status: httpErrorScheme.httpCustomStatusScheme.returnBadRequest,
            message: httpErrorScheme.httpErrorMessage.returnBadRequest
        });
    })();

    const returnInternalServerError = (function () {
        return createHttpError({
            status: httpErrorScheme.httpCustomStatusScheme
                .returnInternalServerError,
            message: httpErrorScheme.httpErrorMessage.returnInternalServerError
        });
    })();

    return {
        returnInternalServerError,
        returnNotFoundAPIUrl,
        returnNoPermission,
        returnBadRequest
    };
})();

この即時関数を呼び出して取得したデータを元にハンドリング処理をしていきます。

テストコード

テストコードを示します。

import { createHttpError } from "@/utils/error/http/http";
import { describe, expect, it } from "vitest";

describe("http", () => {
    it("createHttpErrorのreturnNotFoundAPIUrlが正常に作られる", () => {
        const { returnNotFoundAPIUrl } = createHttpError;
        const error = returnNotFoundAPIUrl;

        expect(error.type).toBe("httpError");
        expect(error.status).toBe(4041);
        expect(error.message).toBe("APIのURLが見つかりません");
    });

    it("createHttpErrorのreturnNoPermissionが正常に作られる", () => {
        const { returnNoPermission } = createHttpError;
        const error = returnNoPermission;

        expect(error.type).toBe("httpError");
        expect(error.status).toBe(4031);
        expect(error.message).toBe("権限がありません");
    });

    it("createHttpErrorのreturnBadRequestが正常に作られる", () => {
        const { returnBadRequest } = createHttpError;
        const error = returnBadRequest;

        expect(error.type).toBe("httpError");
        expect(error.status).toBe(4001);
        expect(error.message).toBe("不正なリクエストです");
    });
});

HttpErrorを拡張してFetchErrorを作成する

ここで、上で紹介したHttpErrorを拡張してfetchでのエラーを返すためのFetchErrorのソースコードを紹介します。

それぞれのソースコードを記載します。
今までのセクションと同じ手順なので説明は割愛します。

スキーム定義

import {
    createHttpScheme,
    HttpCustomStatus,
    HttpCustomStatusScheme,
    HttpErrorStatusErrorMessage,
    HttpErrorStatusResponse
} from "../http/http-scheme";

export interface FetcherErrorStatusScheme extends HttpCustomStatusScheme {
    returnNotSetAPIUrl: 4040;
    returnSchemeError: 5000;
    returnParseError: 8000;
    returnFetchFunctionError: 9000;
    returnUnknownError: 9999;
}

export type FetcherStatus =
    | HttpCustomStatus
    | FetcherErrorStatusScheme["returnNotSetAPIUrl"] // APIのURLが設定されていない
    | FetcherErrorStatusScheme["returnSchemeError"] // スキームエラー
    | FetcherErrorStatusScheme["returnParseError"] // パースエラー
    | FetcherErrorStatusScheme["returnFetchFunctionError"] // フェッチ関数エラー
    | FetcherErrorStatusScheme["returnUnknownError"];

interface FetcherErrorMessageScheme extends HttpErrorStatusErrorMessage {
    returnNotSetAPIUrl: string;
    returnSchemeError: string;
    returnParseError: string;
    returnFetchFunctionError: string;
    returnUnknownError: string;
}

export interface FetchErrorScheme {
    httpErrorStatusResponse: HttpErrorStatusResponse;
    fetcherErrorStatusScheme: FetcherErrorStatusScheme;
    fetchErrorMessage: FetcherErrorMessageScheme;
}

export const fetcherErrorScheme: FetchErrorScheme = (function () {
    const {
        httpCustomStatusScheme,
        httpErrorStatusResponse,
        httpErrorMessage
    } = createHttpScheme;

    /**API仕様で変更 */
    const fetcherErrorStatusScheme: FetcherErrorStatusScheme = {
        ...httpCustomStatusScheme,
        returnNotSetAPIUrl: 4040,
        returnSchemeError: 5000,
        returnParseError: 8000,
        returnFetchFunctionError: 9000,
        returnUnknownError: 9999
    };

    /**API仕様や画面仕様で変更 */
    const fetchErrorMessage: FetcherErrorMessageScheme = {
        ...httpErrorMessage,
        returnNotSetAPIUrl: "APIのURLが設定されていません",
        returnSchemeError: "スキームエラーが発生しました",
        returnParseError: "データのパースに失敗しました",
        returnFetchFunctionError: "フェッチ関数でエラーが発生しました",
        returnUnknownError: "不明なエラーが発生しました"
    };
    return {
        httpErrorStatusResponse,
        fetcherErrorStatusScheme,
        fetchErrorMessage
    };
})();

各エラーのスキームを作成する即時関数を定義

import { CustomError } from "../core/core-error";
import { createHttpError, HttpError } from "../http/http";
import { fetcherErrorScheme, FetcherStatus } from "./fetcher-scheme";

interface FetcherBaseError extends CustomError {
    status: FetcherStatus;
}

export type FetcherError = FetcherBaseError | HttpError;

export const createFetcherError = (function () {
    const {
        returnBadRequest,
        returnNoPermission,
        returnNotFoundAPIUrl,
        returnInternalServerError
    } = createHttpError;
    const {
        fetchErrorMessage,
        fetcherErrorStatusScheme,
        httpErrorStatusResponse
    } = fetcherErrorScheme;

    const createFetcher = ({
        status,
        errorMessage
    }: {
        status: FetcherStatus;
        errorMessage: string;
    }): FetcherError => {
        return {
            type: "fetcherError",
            status,
            message: errorMessage
        };
    };

    const returnNotSetApiUrl = (function () {
        return createFetcher({
            status: fetcherErrorStatusScheme.returnNotSetAPIUrl,
            errorMessage: fetchErrorMessage.returnNotSetAPIUrl
        });
    })();
    const returnSchemeError = (function () {
        return createFetcher({
            status: fetcherErrorStatusScheme.returnSchemeError,
            errorMessage: fetchErrorMessage.returnSchemeError
        });
    })();

    const returnParseError = (function () {
        return createFetcher({
            status: fetcherErrorStatusScheme.returnParseError,
            errorMessage: fetchErrorMessage.returnParseError
        });
    })();

    const returnFetchFunctionError = (function () {
        return createFetcher({
            status: fetcherErrorStatusScheme.returnFetchFunctionError,
            errorMessage: fetchErrorMessage.returnFetchFunctionError
        });
    })();

    const returnUnknownError = (function () {
        return createFetcher({
            status: fetcherErrorStatusScheme.returnUnknownError,
            errorMessage: fetchErrorMessage.returnUnknownError
        });
    })();

    return {
        httpErrorStatusResponse,
        returnNoPermission,
        returnNotFoundAPIUrl,
        returnBadRequest,
        returnUnknownError,
        returnNotSetApiUrl,
        returnSchemeError,
        returnParseError,
        returnFetchFunctionError,
        returnInternalServerError
    };
})();

fetcherに適用

fetcherErrorfetcherに適応したソースコードを紹介します。

import { core, ZodType } from "zod";
import { Option, optionUtility } from "@/utils/option";
import { Result, resultUtility } from "@/utils/result";
import error from "@/utils/error/http";
import {
    createFetcherError,
    FetcherError
} from "@/utils/error/fetcher/fetcher-error";

export async function fetcher<T extends ZodType>({
    url,
    scheme,
    cache
}: {
    url: Option<string>;
    scheme: T;
    cache?: RequestCache;
}): Promise<Result<Option<core.output<T>>, FetcherError>> {
    const { notFound, forbidden, badRequest, internalServerError } =
        error.createHttpScheme.httpErrorStatusResponse;

    const {
        returnNotSetApiUrl,
        returnNotFoundAPIUrl,
        returnNoPermission,
        returnBadRequest,
        returnSchemeError,
        returnUnknownError,
        returnFetchFunctionError,
        returnInternalServerError
    } = createFetcherError;

    const { isNone, createNone, createSome } = optionUtility;
    const { isNG, createNg, createOk, checkPromiseReturn } = resultUtility;

    if (isNone(url)) {
        return createNg(returnNotSetApiUrl);
    }

    const res = await checkPromiseReturn({
        fn: () => fetch(url.value, { cache }),
        err: returnFetchFunctionError
    });

    if (isNG(res)) {
        return res;
    }

    if (!res.value.ok) {
        const status = res.value.status;

        switch (status) {
            case notFound:
                return createNg(returnNotFoundAPIUrl);
            case forbidden:
                return createNg(returnNoPermission);
            case badRequest:
                return createNg(returnBadRequest);
            case internalServerError:
                return createNg(returnInternalServerError);
            default:
                return createNg(returnUnknownError);
        }
    }

    const resValue = await res.value.json();

    const judgeType = scheme.safeParse(resValue);

    if (judgeType.error !== undefined) {
        return createNg(returnSchemeError);
    }

    const okValue = judgeType.data;

    if (okValue === undefined || okValue === null) {
        return createOk(createNone());
    }

    return createOk(createSome(okValue));
}

この関数はPromise<Result<Option<core.output<T>>, FetcherError>>が返却され、エラーの際はFetcherErrorが返却されるようになっています。

一部抜粋すると、

if (!res.value.ok) {
    const status = res.value.status;

    switch (status) {
        case notFound:
            return createNg(returnNotFoundAPIUrl);
        case forbidden:
            return createNg(returnNoPermission);
        case badRequest:
            return createNg(returnBadRequest);
        case internalServerError:
            return createNg(returnInternalServerError);
        default:
            return createNg(returnUnknownError);
    }
}

fetchが成功した際にhttpステータスが200番台以外のものだった時にFetchErrorを返しています。

まとめ

今回のカスタムエラー(Errorクラスを使わない)の利点としては以下のようなものが挙げられます。

  • 型安全
  • エラーの分類ができる
  • IDEにて扱いやすくコーディングできる
  • チーム一貫性が出る
  • neverthrowや自作のResultに適している
    • throwをしないのでスタックトレースの利点がない
    • catch内での明確な分岐処理をしないのであれば特にErrorクラスの意味がない
  • エラーの結果によって、次の処理がしやすい

デメリットとしては最初の構築が大変という面があります。
これらの利点を考えるとプロジェクト立ち上げ時に、API仕様書と要件仕様書を見ながら頑張ってカスタムエラーを作成していた方が良いでしょう。

また、throwcatchでやるという方針でしたらErrorクラスを継承しておいた方が良いです。

neverthrowResultで型の利便性を利用するためにもぜひカスタムエラーを作成してみてください。

それではまた。

Discussion