DanLevy.net

クイズ: 高度なJSエラーマスター

例外は本当に例外的ですか?

JavaScript のエラー、全部把握してると思う?

JSON.stringify(error) は何を返す?

const error = new Error('Oops');
console.log(JSON.stringify(error));

Error オブジェクトは列挙できないプロパティ(message、name、stack)を持つため、JSON.stringify() は {} を返します。これは API 応答でエラーを送る際の典型的な落とし穴です。JSON.stringify(error, Object.getOwnPropertyNames(error)) を使うか、代わりにプレーンオブジェクトを作成してください。

Errorオブジェクトの列挙可能なプロパティに注目してください。

この二つの違いは何ですか?

const err = new Error('Test');
console.log(err);
console.log(JSON.stringify(err));

console.log(err) はエラーメッセージとスタックトレースを表示する。コンソールは Error オブジェクトを特別に扱うからだ。JSON.stringify(err) は '{}' を返す。Error のプロパティは列挙可能ではないためだ。この違いで API デバッグ中に多くの開発者がつまずく。

console.logによる表示とJSONシリアライズを比べてください。

これらのチェックの結果は?

class CustomError extends Error {}
const err = new CustomError('test');
console.log(err instanceof CustomError);
console.log(err instanceof Error);
console.log(err instanceof Object);

すべてのチェックは true を返します。CustomError は Error を継承し、Error は Object を継承しています。instanceof 演算子はプロトタイプチェーン全体を確認するので、CustomError のインスタンスは Error と Object のインスタンスでもあります。

JavaScriptの継承とプロトタイプチェーンを思い出してください。

同一オリジンの iframe 内で作成した Error オブジェクトを、親ウィンドウの変数 iframeError に渡したとします。親ウィンドウの Error を使う次の判定はどうなりますか?

console.log(iframeError instanceof Error);

iframe と親ウィンドウは別々の Error コンストラクタとプロトタイプを持つため、この判定は false です。Object.prototype.toString.call(obj) は別の実行領域のエラーを識別する手掛かりになりますが、Symbol.toStringTag で偽装できるため、本物の Error であることの保証にはなりません。

別の実行領域は別のErrorコンストラクタを持ちます。

文字列をthrowしたらどうなる?

try {
throw "Oops!";
} catch (e) {
console.log(e instanceof Error);
console.log(typeof e);
}

JavaScriptでは任意の値をthrowできる。ここでは e instanceof Error は false で、typeof e は "string" になる。これにより、捕捉した例外がすべてErrorオブジェクトであると想定したエラーハンドリングコードが壊れる可能性がある。デバッグしやすくするため、常にErrorインスタンスをthrowしよう。

JavaScriptではError以外の値もthrowできます。

err.name の値は何ですか?

class CustomError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
const err = new CustomError('test');
console.log(err.name);

err.name は "CustomError" です。なぜなら this.constructor.name がクラス名を返すからです。this.name = this.constructor.name を設定するのは、カスタムエラークラスがスタックトレースやエラーメッセージで正しい名前を表示するようにする一般的なパターンです。

this.constructor.name の評価結果に注目してください。

name を設定しない場合の出力は?

class MyError extends Error {
// No constructor or name setting
}
const err = new MyError('test');
console.log(err.name);

this.name を明示的に設定しないと、エラーは Error クラスからデフォルトの name プロパティ("Error")を継承します。だからカスタムエラークラスではコンストラクタ内で必ず this.name = this.constructor.name を設定すべきです。

Errorのデフォルトのnameプロパティには何が入りますか?

wrapper.cause.message は何を返す?

const original = new Error('Original error');
const wrapper = new Error('Wrapper',
{ cause: original }
);
console.log(wrapper.cause.message);

Error.cause(ES2022)はエラーをチェーンさせ、元のエラーコンテキストを保持できる。wrapper.cause は元のエラーを指すので、wrapper.cause.message は "Original error" を返す。これは下位レベルのエラーを上位コンテキストでラップする際に便利だ。

Error.causeはエラーを連鎖させるための機能です。

V8 / Node.js で、Error.captureStackTrace は何をしますか?

function createError(msg) {
const err = new Error(msg);
Error.captureStackTrace(err, createError);
return err;
}
const error = createError('test');

Error.captureStackTrace(V8/Node.js)は、指定した関数(createError)をスタックトレースから除外し、エラーファクトリ関数をエンドユーザーに見えなくします。これにより、ファクトリが呼び出された場所を指す、よりクリーンなスタックトレースが得られます。

V8固有のスタックトレース操作機能です。

エラーメッセージは何ですか?

function validate(value) {
if (!value) {
throw new Error(
`Value ${value} is invalid`
);
}
}
try {
validate(undefined);
} catch (e) {
console.log(e.message);
}

テンプレートリテラルは補間時に undefined を文字列 "undefined" に変換します。エラーメッセージは "Value undefined is invalid" になります。よりクリーンなメッセージにするには、補間前に value ?? 'null' などのチェックを検討してください。

テンプレートリテラルの補間でundefinedはどうなりますか?

クライアントに何が送られる?

// Express.js route
app.get('/api/data', (req, res) => {
const error = new Error('Database failed');
res.json({ error });
});

res.json() は内部で JSON.stringify() を使用するため、Error オブジェクトは {} になります。クライアントは {"error":{}} を受け取ります。これを修正するには、res.json({ error: error.message }) または res.json({ error: { message: error.message, name: error.name } }) を使用します。

JSON.stringifyによるErrorオブジェクトの扱いを思い出してください。

Promise.reject() は何を受け取れる?

Promise.reject('string').catch(e =>
console.log(typeof e)
);
Promise.reject({code: 404}).catch(e =>
console.log(e.code)
);
Promise.reject(42).catch(e =>
console.log(e)
);

throw と同様に、Promise.reject() は任意の値(文字列、オブジェクト、数値など)を受け取れます。これにより "string"、404、42 が出力されます。Promise チェーンで捕捉した値の型は常に確認しましょう。特にサードパーティのコードが Error 以外で拒否する可能性がある場合は注意が必要です。

Promiseの拒否値はthrowと同様に任意の値です。

error.code と error.errno の信頼性はどれくらいですか?

const fs = require('fs');
fs.readFile('missing.txt', (err, data) => {
if (err) {
console.log(err.code); // 'ENOENT'
console.log(err.errno); // -2
}
});

code や errno のようなプロパティは環境固有(この場合は Node.js)で、標準の Error オブジェクトには含まれません。ブラウザのエラーにはこれらのプロパティはありません。常に存在を確認しましょう:if (err.code === 'ENOENT') のように、存在すると仮定しないでください。

JavaScriptの実行環境とエラーの種類に注目してください。

これらのチェックは何を返す?

const fakeError = {
name: 'Error',
message: 'Fake error',
stack: 'fake stack'
};
console.log(fakeError instanceof Error);
console.log(Object.prototype.toString.call(
fakeError
) === '[object Error]');

このコードは false, false を出力します。fakeError は Error のプロトタイプを継承しないプレーンオブジェクトで、Object.prototype.toString.call(fakeError) は "[object Object]" を返します。ただし Symbol.toStringTag を設定するとタグを偽装できるため、この方法は一般的な真正性の検証にはなりません。

一方はプロトタイプチェーンをたどり、もう一方は Symbol.toStringTag の影響を受けるオブジェクトタグを返します。

エラーハンドリングの極意

シリアライズの落とし穴からクロスコンテキストの instanceof 失敗まで、こういった高度な概念がジュニア開発者と、​すでに​傷だらけの​ベテラン​を分けます。

もっと挑戦したいですか? 追加の JavaScript、アルゴリズム、その他の脳トレが揃った 全クイズコレクション をチェックしてください。