Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: DisplayableErrorを追加し、テストも追加する #2559

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 115 additions & 9 deletions src/helpers/errorHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,121 @@ export const ensureNotNullish = <T>(
return value;
};

/** エラーからエラー文を作る */
/**
* ユーザーに表示するメッセージを持つエラー。
* {@link errorToMessages}でユーザー向けのメッセージとして扱われる。
*/
export class DisplayableError extends Error {
constructor(userFriendlyMessage: string, { cause }: { cause?: Error } = {}) {
super(userFriendlyMessage, { cause });
this.name = "DisplayableError";
}
}

/**
* 例外からユーザー向けのエラーメッセージと内部向けのエラーメッセージを作る。
* DisplayableErrorのメッセージはユーザー向けとして扱う。
* それ以外の例外のメッセージは内部向けとして扱う。
* causeやAggregateErrorの場合は再帰的に処理する。
* 再帰的に処理する際、一度DisplayableError以外の例外を処理した後は内部向けとして扱う。
*/
export const errorToMessages = (
e: unknown,
): { displayable: string[]; internal: string[] } => {
const errors = flattenErrors(e);
const { displayable, internal } = splitErrors(errors);
return {
displayable: toMessages(displayable),
internal: toMessages(internal),
};

function flattenErrors(e: unknown): unknown[] {
const errors = [e];
if (e instanceof AggregateError) {
errors.push(...e.errors.flatMap(flattenErrors));
}
if (e instanceof Error && e.cause) {
errors.push(...flattenErrors(e.cause));
}
return errors;
}

function splitErrors(errors: unknown[]): {
displayable: unknown[];
internal: unknown[];
} {
const firstInternalErrorIndex = errors.findIndex(
(error) => !(error instanceof DisplayableError),
);
const splitIndex =
firstInternalErrorIndex === -1 ? errors.length : firstInternalErrorIndex;

return {
displayable: errors.slice(0, splitIndex),
internal: errors.slice(splitIndex),
};
}

function toMessages(errors: unknown[]): string[] {
const messages: string[] = [];
for (const e of errors) {
if (e instanceof Error) {
let message = "";
if (!["Error", "AggregateError", "DisplayableError"].includes(e.name)) {
message += `${e.name}: `;
}
message += e.message;
messages.push(message);
continue;
}

if (typeof e === "string") {
messages.push(`Unknown Error: ${e}`);
continue;
}

if (typeof e === "object" && e != undefined) {
messages.push(`Unknown Error: ${JSON.stringify(e)}`);
continue;
}

messages.push(`Unknown Error: ${String(e)}`);
}
return messages;
}
};

/**
* 例外からエラーメッセージを作る。
* {@link errorToMessages}の結果を結合して返す。
* 長い場合は後ろを切る。
*/
export const errorToMessage = (e: unknown): string => {
if (e instanceof Error) {
return `${e.toString()}: ${e.message}`;
} else if (typeof e === "string") {
return `String Error: ${e}`;
} else if (typeof e === "object" && e != undefined) {
return `Object Error: ${JSON.stringify(e).slice(0, 100)}`;
} else {
return `Unknown Error: ${String(e)}`;
const { displayable, internal } = errorToMessages(e);
const messages = [...displayable];
if (internal.length > 0) {
messages.push("(内部エラーメッセージ)", ...internal);
}
return trim(messages.join("\n"));

function trim(str: string) {
return trimLongString(trimLines(str));
}

function trimLines(str: string) {
// 15行以上ある場合は15行までにする
const lines = str.split("\n");
if (lines.length > 15) {
return lines.slice(0, 15 - 1).join("\n") + "\n...";
}
return str;
}

function trimLongString(str: string) {
// 300文字以上ある場合は300文字までにする
if (str.length > 300) {
return str.slice(0, 300 - 3) + "...";
}
return str;
}
};
98 changes: 98 additions & 0 deletions tests/unit/lib/errorHelper.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, it, expect } from "vitest";
import { DisplayableError, errorToMessage } from "@/helpers/errorHelper";

describe("errorToMessage", () => {
it("Errorインスタンス", () => {
const input = new Error("error instance");
const expected = "(内部エラーメッセージ)\nerror instance";
expect(errorToMessage(input)).toEqual(expected);
});

it("SyntaxErrorインスタンス", () => {
const input = new SyntaxError("syntax error instance");
const expected =
"(内部エラーメッセージ)\nSyntaxError: syntax error instance";
expect(errorToMessage(input)).toEqual(expected);
});

it("自作エラーインスタンス", () => {
class CustomError extends Error {
constructor(message: string) {
super(message);
this.name = "CustomError";
}
}
const input = new CustomError("custom error instance");
const expected =
"(内部エラーメッセージ)\nCustomError: custom error instance";
expect(errorToMessage(input)).toEqual(expected);
});

it("AggregateErrorインスタンス", () => {
const input = new AggregateError(
[new Error("error1"), new Error("error2")],
"aggregate error",
);
const expected =
"(内部エラーメッセージ)\naggregate error\nerror1\nerror2";
expect(errorToMessage(input)).toEqual(expected);
});

it("cause付きエラーインスタンス", () => {
const input = new Error("error instance", { cause: new Error("cause") });
const expected = "(内部エラーメッセージ)\nerror instance\ncause";
expect(errorToMessage(input)).toEqual(expected);
});

it("文字列エラー", () => {
const input = "string error";
const expected = "(内部エラーメッセージ)\nUnknown Error: string error";
expect(errorToMessage(input)).toEqual(expected);
});

it("オブジェクトエラー", () => {
const input = { key: "value" };
const expected = '(内部エラーメッセージ)\nUnknown Error: {"key":"value"}';
expect(errorToMessage(input)).toEqual(expected);
});

it("不明なエラー", () => {
const input = undefined;
const expected = "(内部エラーメッセージ)\nUnknown Error: undefined";
expect(errorToMessage(input)).toEqual(expected);
});

it("DisplayableErrorインスタンス", () => {
const input = new DisplayableError("displayable error instance");
const expected = "displayable error instance";
expect(errorToMessage(input)).toEqual(expected);
});

it("DisplayableError -> Error", () => {
const input = new DisplayableError("displayable error instance", {
cause: new Error("cause"),
});
const expected =
"displayable error instance\n(内部エラーメッセージ)\ncause";
expect(errorToMessage(input)).toEqual(expected);
});

it("DisplayableError -> DisplayableError", () => {
const input = new DisplayableError("displayable error instance", {
cause: new DisplayableError("displayable cause"),
});
const expected = "displayable error instance\ndisplayable cause";
expect(errorToMessage(input)).toEqual(expected);
});

it("DisplayableError -> Error -> DisplayableError", () => {
const input = new DisplayableError("displayable error instance", {
cause: new Error("cause", {
cause: new DisplayableError("displayable cause"),
}),
});
const expected =
"displayable error instance\n(内部エラーメッセージ)\ncause\ndisplayable cause";
expect(errorToMessage(input)).toEqual(expected);
});
});
Loading