エラーをチェインするライブラリと ES2022 Error Cause
追記情報
【2023/04/29 追記】
各エンジンのコンソールへへの表示について、古くなっている記述を修正。
【2021/10/27 追記】
2021年10月の TC39 meeting で Stage 4 になったため記事のタイトル、一部内容を更新しています。
エラーを識別するモチベーション
デバッグを容易にするためにエラーを識別したいことがあります。例えば Fetch API を二回実行した際にエラーが投げられてしまうことを考えてみます。
async function fetchData() {
// データを取得するために Cookie にセッションを登録
await fetch("/api/auth");
// データを取得する
const response = await fetch("/api/data");
return await response.json();
}
let data;
try {
data = await fetchData();
} catch (e) {
// TypeError: Failed to fetch
// at fetch
console.error(e);
}
このときにどの Fetch API からエラーが投げられたのかわかりにくい問題があります。かといって以下のように握りつぶしてしまうと肝心のエラーの中身がわからなくなってしまいます。
async function fetchData() {
// データを取得するために Cookie にセッションを登録
try {
await fetch("/api/auth");
} catch {
throw new Error("Failed to auth");
}
// データを取得する
try {
const response = await fetch("/api/data");
return await response.json();
} catch {
throw new Error("Failed data fetch");
}
}
let data;
try {
data = await fetchData();
} catch (e) {
// Error: Failed to auth
// at fetchData
console.error(e);
}
エラーをチェインするライブラリ
エラーオブジェクトをラップして新たなエラーオブジェクトを作ることが出来るライブラリがあります。これらを使うことでエラーの識別が容易になります。
extensible-custom-error
カスタムエラーを作ることが出来るライブラリです。コンストラクタの第二引数にエラーオブジェクトを入れることで stack
プロパティを上書きし、内部エラーとマージさせます。それによって通常の実装においてコンソール上で内部エラーのスタックトレースも見ることが出来ます。
import ExtensibleCustomError from "extensible-custom-error";
class CustomError extends ExtensibleCustomError {}
async function fetchData() {
// データを取得するために Cookie にセッションを登録
try {
await fetch("/api/auth");
} catch (e) {
throw new CustomError("Failed to auth", e);
}
// データを取得する
try {
const response = await fetch("/api/data");
return await response.json();
} catch (e) {
throw new CustomError("Failed data fetch", e);
}
}
let data;
try {
data = await fetchData();
} catch (e) {
// CustomError: Failed to auth
// at fetchData
// TypeError: Failed to fetch
// at fetch
console.error(e);
}
TraceError.js
TraceError
クラスを提供するライブラリです。第二引数にエラーオブジェクトを入れることで内部エラーを保持することができます。こちらも stack
プロパティを上書きするため、コンソールで見やすくなります。
import TraceError from 'trace-error';
async function fetchData() {
// データを取得するために Cookie にセッションを登録
try {
await fetch("/api/auth");
} catch (e) {
throw new TraceError("Failed to auth", e);
}
// データを取得する
try {
const response = await fetch("/api/data");
return await response.json();
} catch (e) {
throw new TraceError("Failed data fetch", e);
}
}
let data;
try {
data = await fetchData();
} catch (e) {
// TraceError: Failed to auth
// at fetchData
// TypeError: Failed to fetch
// at fetch
console.error(e);
}
ES2022 Error Cause
仕様で定義されているエラーオブジェクトのコンストラクタにオプションとして内部エラーを受け取れる拡張を行うのが ES2022 Error Cause です。前述したライブラリとは異なりこの仕様では stack
プロパティについては特に言及せず、単に cause
プロパティによって内部エラーにアクセス出来るというものとなっています。
async function fetchData() {
// データを取得するために Cookie にセッションを登録
try {
await fetch("/api/auth");
} catch (e) {
throw new Error("Failed to auth", { cause: e });
}
// データを取得する
try {
const response = await fetch("/api/data");
return await response.json();
} catch (e) {
throw new Error("Failed data fetch", { cause: e });
}
}
let data;
try {
data = await fetchData();
} catch (e) {
// Error: Failed to auth
// at fetchData
console.error(e);
// TypeError: Failed to fetch
// at fetch
console.error(e.cause);
}
この提案はコンストラクタを拡張するものではありますが、実質的にエラーオブジェクトに cause
プロパティを与えているのと変わりはありません。どちらかといえばエラーオブジェクトは内部エラーを cause
プロパティとして持つものとして定めたという側面が強いです。
try {
// some code
} catch (e) {
// 実質同じ
const error = new Error("Foo Error");
error.cause = e;
throw error;
}
実装環境にもよりますが、今のところコンソール上で内部エラー情報を表示したい場合は明示的に cause
プロパティにアクセスする必要があります。コンソールへの表示内容は実装依存[1]なので規定できませんが、これは将来的に各実装が対応することによって解決されるものかと思われます。
2023年04月現在、ブラウザの中では Firefox のみ cause
プロパティにアクセスする必要なく内部エラーの情報も表示してくれます。
Node.js では v16.9.0 からコンストラクタのオプションへの対応が入り、v16.14.0 からコンソールに表示されるようになりました。
また Deno では v1.13.0 からコンストラクタのオプションへの対応が入り、v1.18.0 からコンソールに表示されます。
stack
の挙動に言及しないのか
何故仕様で 実はスタックトレース及びエラーオブジェクトの stack
プロパティは各エンジンによる独自実装です。ECMAScript には含まれていません。互換性の問題により仕様で stack
プロパティの挙動について規定することができず、単に cause
プロパティを追加するだけの提案になったわけです。
スタックトレースが独自実装なのはそれはそれで問題なため、仕様側でちゃんと規定する提案が Stage 1 Error Stacks です。基本的にはエラーオブジェクトを受け取ってスタックトレースを返す函数を追加する提案となっており、サブとして Web Reality に合うように Annex B に Error#stack
を追加するものとなっています。
Annex B についての詳細は別の記事があるのでこちらを御覧ください。
余談ですが V8 は非標準の Stack trace API を持っています。もちろんこれは Chromium 系ブラウザや Node.js そして Deno でのみ扱えます。
内部エラーをマージしたスタックトレース文字列を生成する
Error Cause の ponyfill である Pony Cause が提供する stackWithCauses
函数のように、cause
プロパティを辿りスタックトレースを組み立てるコードが必要になります。
素朴に実装してみると以下のようになるかなと思います。
function stackWithCause(error: Error): string {
const causes: Error[] = [];
let temp = error;
while (
temp.cause instanceof Error &&
temp.cause !== error && !causes.includes(temp.cause) // circular check
) {
causes.push(temp.cause);
temp = temp.cause;
}
return `${error.stack}${
causes.map((cause) => `\nCaused by ${cause.stack}`).join("")
}`;
}
もちろん現状非標準である stack
プロパティに依存してしまうのでうまく動作しなくなってしまう可能性はあります。しかしその非標準の上に成り立っているスタックトレースを操作するライブラリで溢れている現代において、破壊的な変更はそうそう起きないだろうと思うので基本的には大丈夫でしょう[2]。
それはそうとして言語仕様側で持っていて欲しい機能ではあるので、時期尚早ですが将来のために issue をたてました。
Sentry での扱い
エラーを監視する Sentry の Official JavaScript SDK である sentry-javascript では、デフォルトのインテグレーション LinkedErrors
によって既にエラーオブジェクトが内部エラーを cause
プロパティとして持つものとして独自に扱っています。つまり特に設定する必要なく内部エラーもログに残すことが出来ます。
結び
今回は Error Cause を紹介してみました。スタックトレースが実装依存なため少し扱いにくいところはありますが、言語仕様に内部エラーを保持する方法が提供されるのは便利なのかと思います。
ECMAScript の提案を追うのは割と楽しいので皆さんもよかったらどうぞ。
-
Console API は WHATWG の Console Standard で定義されています。表示内容が実装依存なのはここに記されています。 ↩︎
-
今のところ ES2022 Error Cause を実装した JavaScript エンジンにおいて、
stack
プロパティに内部エラーのスタックトレースをマージさせるような実装はされていません。もしされてしまった場合この方法だとダブルプリンティングが発生してしまいますがきっと大丈夫でしょう。 ↩︎
Discussion