🚨

【JavaScript】エラー処理 完全ガイド

2020/09/21に公開

error handling in javascript

本記事は、Valentino Gagliardi 氏の "A mostly complete guide to error handling in JavaScript." を許可を頂いた上で翻訳したものです。

TOC

プログラミングにおけるエラーとは?

私たちの書くプログラムは 常にうまく動作するわけではありません。

時に、プログラムを停止させたり、ユーザーに何か問題が起こったことを知らせたい シチュエーションがあります。

例えば、以下のようなケースがあるでしょう:

  • プログラムが存在しないファイルを開こうとした
  • ネットワークの接続が不調である
  • ユーザーが無効な値を入力した

すべてのケースで、私たちがプログラマーとして、またはプログラミングエンジンを通して、 エラー を作成します。

エラーを作成することで、ユーザーに問題が起きたことをメッセージで伝えたり、プログラムの実行を停止させたりできるのです。

JavaScript におけるエラーとは?

JavaScript におけるエラーはオブジェクト です。このオブジェクトは、後にプログラムを停止するために 投げられる ものです。

JavaScript で新しくエラーを作成するには、適切な コンストラクタ関数 を呼び出します。例えば、一般的なエラーを新規に作成するには以下を実行します:

const err = new Error("Something bad happened!");

new というキーワードを省略することもできます:

const err = Error("Something bad happened!");

一度作成されると、エラーオブジェクトは3つのプロパティを提供します。

  • message: エラーメッセージを含む文字列
  • name: エラーのタイプ
  • stack: 関数実行のスタックトレース

例えば、適当なメッセージ文字列でTypeError オブジェクトを作成した場合、message は実際に渡した文字列となり、name"TypeError"となります:

const wrongType = TypeError("Wrong type given, expected number");

wrongType.message; // "Wrong type given, expected number"
wrongType.name; // "TypeError"

Firefox は上記のプロパティの他に、columnNumberfilenamelineNumberといった非標準プロパティを実装しています。

JavaScript エラー型の種類

JavaScript にはたくさんのエラー型があります。具体的には以下の通りです:

  • Error
  • EvalError
  • InternalError
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError

これらのエラー型は、あたらしいエラーオブジェクトを返す 本物のコンストラクタ関数 であることを忘れないでください。

あなた自身のエラーオブジェクトを作成する際、ErrorTypeError という最も一般的な 2 つのエラー型を使うことが多いでしょう。

エラーの大多数は InternalErrorSyntaxError のように、JavaScript エンジンから直接的に発現するものがほとんどです。

TypeError の一例は、const に再代入しようとした際に発生します:

const name = "Jules";
name = "Caty";

// TypeError: Assignment to constant variable.

SyntaxError の一例は、タイプミスをしたときに発生します:

va x = '33';
// SyntaxError: Unexpected identifier

または、awaitasync 関数以外で利用するなど、予約語を不適切な場所を使った場合にも発生します:

function wrong(){
    await 99;
}

wrong();

// SyntaxError: await is only valid in async function

TypeErrorの他の例としては、ページに存在しない HTML 要素を指定したときに発生します:

Uncaught TypeError: button is null

これらのよくあるエラーオブジェクトに加えて、AggregateError オブジェクトが JavaScript にもうすぐ導入される予定です。後ほど見るように、AggregateError は複数のエラーをまとめる際に便利です。

これらの組み込みエラーに加えて、ブラウザでは以下のようなもの目にすることがあります:

  • DOMException
  • DOMError (Dupulicated, 今は使われていない)

DOMException は Web APIs に関連するエラーファミリーです。ブラウザの中で、ばかげたことをしたときに投げられます。例えば以下のようなことです:

document.body.appendChild(document.cloneNode(true));

結果:

Uncaught DOMException: Node.appendChild: May not add a Document as a child

完全なリストは、MDNのこちらのページを参照してください。

例外とは?

多くのデベロッパーは、エラーと例外を同様のものとして考えています。実際には、 エラーオブジェクトが投げられたときにのみ、エラーオブジェクトが例外になる のです。

JavaScript で例外を投げるには、throwとエラーオブジェクトを用います:

const wrongType = TypeError("Wrong type given, expected number");

throw wrongType;

短縮形のほうがより一般的です。多くのコードベースで以下のようなものを目にするでしょう:

throw TypeError("Wrong type given, expected number");

または

throw new TypeError("Wrong type given, expected number");

関数や条件分岐構文の外で例外を投げることはほとんどありません。代わりに、以下の例を考えてみましょう:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

ここでは、関数の引数が文字列(string)かどうかをチェックしています。文字列でなければ、例外を投げます。

JavaScript のルール的には、エラーオブジェクトだけではなく何でも投げることができます:

throw Symbol();
throw 33;
throw "Error!";
throw null;

しかしながら、 プリミティブ型を投げることは避け、適切はエラーオブジェクトを投げるべき です。

そうすることで、コードベースにおいてエラー処理の一貫性を保つことができます。他のチームメンバーがエラーオブジェクトにおいて error.messageerror.stack にアクセスすることができます。

例外を投げると何が起きる?

例外はエレベーターが上に行くようなものです。 一度例外を投げると、どこかで止められない限りプログラムスタックの中でぶくぶくと泡立ってしまします。

以下のようなコードを考えてみましょう:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase(4);

このコードをブラウザもしくは Node.js で実行した場合、プログラムは停止し以下のようなエラーを表示します:

Uncaught TypeError: Wrong type given, expected a string
    toUppercase http://localhost:5000/index.js:3
    <anonymous> http://localhost:5000/index.js:9

さらに、エラーが発生した正確な行数を把握することができます。

この表示が スタックトレース であり、プログラムの問題を追跡する際に便利です。

スタックトレースは下から上に積み上がります。つまりここでは以下のようになっていました:

    toUppercase http://localhost:5000/index.js:3
    <anonymous> http://localhost:5000/index.js:9

ここから以下のことが言えます:

  • 9 行目にあるプログラムの何かが toUppercase を呼び出した
  • 3 行目において toUppercase で問題が発生した

ブラウザのコンソールで確認する以外にも、エラーオブジェクトの stack プロパティにアクセスすることによってスタックトレースを見ることができます。

もし例外が キャッチされなかった 場合、つまり、プログラマが例外をキャッチするために何もしなかった場合、プログラムはクラッシュします。

コードの中で、いつ、どこで例外をキャッチするかは、その時々で異なります。

例えば、 プログラムを完全にクラッシュさせるために、例外をスタックに加えて伝播させたいかもしれません。 これは、無効なデータで処理を進めるよりもプログラムを停止させたほうが安全である、といった、致命的なエラーを処理する際に起こりうることです。

さて、ここまでで基本の紹介をしたので、 JavaScript の同期処理と非同期処理における、エラーと例外処理 に話を進めましょう。

同期エラー処理

同期処理のコードはほとんどの場合単純でわかりやすいので、エラー処理も簡単です。

通常関数のエラー処理

同期処理のコードは、書かれた通りに順番に実行されます。前述のコードをもう一度見てみましょう:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase(4);

ここで、JavaScript エンジンは toUppercase を呼び出して実行しています。すべての処理は 同期的 に行われます。このように同期関数から発生する例外を キャッチ するには、try/catch/finally を使うことができます:

try {
  toUppercase(4);
} catch (error) {
  console.error(error.message);
  // or log remotely
} finally {
  // clean up
}

通常、try はハッピーパスや、潜在的に例外を投げる可能性のある関数呼び出しに対して利用します。

catch は、 実際に例外を捉えます。 エラーオブジェクトを受け取り 、エラーの内容を検査することができます(そして本番環境ではログをリモートサーバーに送信したりします)。

一方で、finally ステートメントは、関数の実行結果に関わらず実行されます。つまり、関数が失敗したか成功したかにかかわらず finally 内に書かれたコードは実行されます。

try/catch/finally同期的な 構造であることを覚えておいて下さい。そしていま、 非同期処理のコードから発生する例外をキャッチする方法を獲得した のです。

ジェネレーター関数のエラー処理

JavaScript におけるジェネレーター関数は、関数の特殊な形式です。

この形式の関数は、関数の内側のスコープとその外側の間で 双方向のコミュニケーションチャネル を提供する以外に、 任意に停止したり再開したり することができます。

ジェネレーター関数を作成するには、function キーワードの後ろにアスタリスク * を付けます:

function* generate() {
//
}

そうすると、値を返すために関数内で yield を使用することができます:

function* generate() {
  yield 33;
  yield 99;
}

ジェネレーター関数の返り値イテレータオブジェクト です。ジェネレーターから値を取り出すためには、2つの方法があります:

  • イテレータオブジェクトの next() を呼び出す
  • for...ofイテレーション する

先程の例で、ジェネレーターから値を取り出す場合は、以下のようにできます:

function* generate() {
  yield 33;
  yield 99;
}

const go = generate();

ここで go がイテレータオブジェクトになります。

ここから、go.next() を呼び出し、実行を進めることができます:

function* generate() {
  yield 33;
  yield 99;
}

const go = generate();

const firstStep = go.next().value; // 33
const secondStep = go.next().value; // 99

ジェネレーターは、 呼び出し元から値や例外を受け取ることもできます。

next() に加えて、ジェネレーターから返されたイテレータオブジェクトは、throw() メソッドを持っています。

このメソッドを利用して、ジェネレーターに例外を注入することによってプログラムを停止させてみましょう:

function* generate() {
  yield 33;
  yield 99;
}

const go = generate();

const firstStep = go.next().value; // 33

go.throw(Error("Tired of iterating!"));

const secondStep = go.next().value; // never reached

注入された例外をキャッチするには、ジェネレーター関数内の処理を try/catch 構文で囲む必要があります(必要であれば finally も利用できます):

function* generate() {
  try {
    yield 33;
    yield 99;
  } catch (error) {
    console.error(error.message);
  }
}

ジェネレーター関数は例外を関数の外に投げることもできます。この仕組みは、try/catch/finally を使って同期処理の例外をキャッチするものと同じです。

ジェネレーター関数に対して for...of 構文を利用する例は以下のとおりです:

function* generate() {
  yield 33;
  yield 99;
  throw Error("Tired of iterating!");
}

try {
  for (const value of generate()) {
    console.log(value);
  }
} catch (error) {
  console.error(error.message);
}

/* Output:
33
99
Tired of iterating!
*/

ここでは、try ブロックの中でハッピーパスを実行し、例外があれば catch でキャッチします。

非同期エラー処理

JavaScript はシングルスレッドで実行されるプログラム言語であり、原理的には同期的です。

ブラウザエンジンのようなホスト環境が JavaScript の機能を拡張させたことで、外部のシステムと通信したり、I/O 処理を行うための、たくさんの Web API が使えるようになりました。

ブラウザにおける非同期性の例は タイムアウト(timeouts)、イベント(events)、プロミス(Promise) があります。

非同期の世界におけるエラー処理 は同期の世界におけるそれとは異なります。

いくつか例を見ていきましょう。

タイマーのエラー処理

JavaScript を学び始めたばかりのとき、try/catch/finally 構文について学ぶと、あらゆるコードブロックを try/catch/finally 構文 で囲みたくなるかもしれません。

例えば以下のような関数を考えてみましょう:

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000)
}

この関数は、約 1 秒後にエラーを投げます。この例外を正しく扱うにはどうしたらよいでしょうか?

以下のようなコードは 上手く動きません :

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000);
}

try {
  failAfterOneSecond();
} catch (error) {
  console.error(error.message);
}

前述したように、try/catch 構文は同期的です。一方で、ここでは setTiemout という、タイマー機能を持つブラウザの API を利用しています。

setTimeout に渡したコールバック関数が実行されるときには、既にtry/catch 構文の実行は 終わっている のです。上のプログラムは例外をキャッチすることができず、クラッシュしてしまいます。

2 つの異なったトラックが実行されているのです:

Track A: --> try/catch
Track B: --> setTimeout --> callback --> throw

プログラムをクラッシュさせたくなければ、try/catch 構文を、setTimeout に渡しているコールバック関数の中に移動する必要があります。

しかし、このアプローチは多くの場合意味を成しません。後で見るように、 Promises を用いた非同期エラー処理がより優れている のです。

イベントのエラー処理

Document Object Model (DOM) の HTML ノードは、EventTarget と連携しています。EventTarget は、ブラウザにおけるあらゆるイベントエミッターの共通の祖先といえる存在です。

これはつまり、ページ上の全ての HTML 要素におけるイベントを取得することができることを意味します。

(Node.js も今後のリリースで EventTarget をサポートする予定です)

DOM イベントに対するエラー処理の仕組みは、非同期 Web API における仕組みと同様 です。

以下の例を考えてみましょう:

const button = document.querySelector("button");

button.addEventListener("click", function() {
  throw Error("Can't touch this button!");
});

ここでは、ボタンがクリックされた瞬間に例外を投げています。どのようにその例外をキャッチするのでしょうか?以下のパターンは 上手く動作せず 、プログラムはクラッシュしてしまいます:

const button = document.querySelector("button");

try {
  button.addEventListener("click", function() {
    throw Error("Can't touch this button!");
  });
} catch (error) {
  console.error(error.message);
}

setTimeout の例で見たように、addEventListener に渡されるあらゆるコールバック関数は、非同期的に実行されます:

Track A: --> try/catch
Track B: --> addEventListener --> callback --> throw

プログラムをクラッシュさせたくなければ、addEventListener のコールバック関数内部に try/catch 構文を移動する必要があります。

しかしここでも、そのようにする意味がほぼありません。

setTimeout の例で見たように、非同期処理コードの実行パスにおいて投げられた例外は 外側でキャッチすることができるものではなく 、結果としてプログラムはクラッシュします。

次のセクションで、Promises と async/await がどのように非同期処理におけるエラー処理を手軽なものにするか見ていきます。

onerror はどうだろう?

HTML 要素には、onlickonmouseenteronchange など多くのイベントハンドラがあります。

そのなかには、onerror もありますが、throw やその類のものとは何も関係がありません。

onerror イベントハンドラは、<img><script> のような HTML 要素が存在しないリソースを扱ったときにトリガーされます。

以下のような例を考えてみましょう:

// omitted
<body>
<img src="nowhere-to-be-found.png" alt="So empty!">
</body>
// omitted

上記のような、存在しないリソースを参照する要素を含んだ HTML ドキュメントをブラウザで見ると、コンソールに以下のようなエラーが表示されます:

GET http://localhost:5000/nowhere-to-be-found.png
[HTTP/1.1 404 Not Found 3ms]

JavaScript では、このエラーを以下のように「キャッチ」できるかもしれません:

const image = document.querySelector("img");

image.onerror = function(event) {
  console.log(event);
};

より優れた形で書くと、以下のようになります:

const image = document.querySelector("img");

image.addEventListener("error", function(event) {
  console.log(event);
});

このパターンは、画像やスクリプトなどのリソースに欠損があった際に、代替となるリソースをローディングしたい場合 に便利です。

だたし、onerrorthrowtry/catch とは何の関係もないことを覚えておいて下さい。

Promise を用いたエラー処理

Promise によるエラー処理を説明するために、何度も登場している以下の例を「約束化(promisify)」させてみましょう。以下のコード例を編集していきます:

function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase(4);

単純に文字列もしくは例外を返す代わりに、成功とエラーを処理するための Promise.rejectPromise.resolve を利用してみましょう:

function toUppercase(string) {
  if (typeof string !== "string") {
    return Promise.reject(TypeError("Wrong type given, expected a string"));
  }

  const result = string.toUpperCase();

  return Promise.resolve(result);
}

(厳密には、上記コードに非同期処理を行う部分はありませんが、説明するには十分です)

いま、toUppercase 関数は「約束」され、処理結果を扱うために then を、 リジェクトされた Promise を処理するためcatch を使うことができます:

toUppercase(99)
  .then(result => result)
  .catch(error => console.error(error.message));

上記のコードは、以下のようなログを吐き出します:

Wrong type given, expected a string

Promise において、catch はエラーを処理するための構成要素です。

catchthen に加え、finally もあります。この finally は、try/catch 構文における finally と似たものです。

Promise における finally も、返された Promise の結果に 関わらず 実行されます:

toUppercase(99)
  .then(result => result)
  .catch(error => console.error(error.message))
  .finally(() => console.log("Run baby, run"));

then/catch/finally に渡されたコールバック関数は、Microtask キューによって非同期に処理されることを覚えておいて下さい。これらは、イベントやタイマーよりも優先される micro task です。

プロミス(Promise)、エラー(error)そしてスロー(throw)

Promise をリジェクトする際は、引数としてエラーオブジェクト渡すのが ベストプラクティス です:

Promise.reject(TypeError("Wrong type given, expected a string"));

そうすることで、エラー処理の一貫性を保つことができます。他のチームメンバーが常に error.message にアクセスすることができますし、さらに重要なことに、スタックトレースを調査することができます。

Promise.reject に加えて、例外を投げることで Promise チェーンから抜け出すことができます。

以下のコード例を考えてみます:

Promise.resolve("A string").then(value => {
  if (typeof value === "string") {
    throw TypeError("Expected a number!");
  }
});

文字列を返すとともに Promise をリゾルブし、そしてその直後に throw によって例外を投げています。

例外の伝播を食い止めるために、通常通り catch を使うことができます:

Promise.resolve("A string")
  .then(value => {
    if (typeof value === "string") {
      throw TypeError("Expected a number!");
    }
  })
  .catch(reason => console.log(reason.message));

このパターンは、fetch を使う際によく用いられます。レスポンスオブジェクトのエラーチェックを行う例は以下の通りです:

fetch("https://example-dev/api/")
  .then(response => {
    if (!response.ok) {
      throw Error(response.statusText);
    }

    return response.json();
  })
  .then(json => console.log(json));

ここでも、catch によって例外を受け取ることができます。もし例外を受け取ることに失敗した場合、あるいはあえて受け取らないことにした場合、 例外はキャッチされるまでスタックに残り続けます。

これは一概に悪いこととは言えませんが、環境によって、キャッチされていないリジェクトに対する挙動は異なります。

例えば、Node.js は将来的に、処理されていない Promise のリジェクトがあった場合は、プログラムをクラッシュさせる予定です:

DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

必ずリジェクトはキャッチしましょう!

"プロミス化"されたタイマーのエラー処理

タイマーとイベントにおいて、コールバック関数内で投げられた例外をキャッチすることは不可能ではありません。前のセクションで、以下のような例を挙げました:

function failAfterOneSecond() {
  setTimeout(() => {
    throw Error("Something went wrong!");
  }, 1000);
}

// DOES NOT WORK
try {
  failAfterOneSecond();
} catch (error) {
  console.error(error.message);
}

Promise によって与えられた解決策は、コードの「プロミス化」です。基本的に、Promise でタイマーを囲みます:

function failAfterOneSecond() {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(Error("Something went wrong!"));
    }, 1000);
  });
}

reject によって Promise のリジェクトをセットし、エラーオブジェクトを渡します。

この時点で、catch をつかって例外を処理することができます:

failAfterOneSecond().catch(reason => console.error(reason.message));

Tips: 成功した Promise の返り値の変数名として value、Promise のリジェクトの変数名として reason を使うことが一般的です。

Node.js は promisify と呼ばれる、古い形で書かれたコールバック API をプロミス化するユーティリティを提供しています。

Promise.all のエラー処理

Promise の static メソッドである Promise.all は Promise の配列を引数にとり、リゾルブした Promise の配列を返します。

const promise1 = Promise.resolve("All good!");
const promise2 = Promise.resolve("All good here too!");

Promise.all([promise1, promise2]).then((results) => console.log(results));

// [ 'All good!', 'All good here too!' ]

渡した配列のどれか1つでもリジェクトされた場合、Promise.all は最初にリジェクトされた Promise のエラーとともにリジェクトします。

このような状況を扱うために、前のセクションで見たように catch が利用できます:

const promise1 = Promise.resolve("All good!");
const promise2 = Promise.reject(Error("No good, sorry!"));
const promise3 = Promise.reject(Error("Bad day ..."));

Promise.all([promise1, promise2, promise3])
  .then(results => console.log(results))
  .catch(error => console.error(error.message));

Promise.all の実行結果に関わらず関数を実行するには、finally を利用します:

Promise.all([promise1, promise2, promise3])
  .then(results => console.log(results))
  .catch(error => console.error(error.message))
  .finally(() => console.log("Always runs!"));

Promise.any のエラー処理

Promise.any (Firefox > 79, Chrome > 85) は、Promise.all の反対の処理をする関数と考えることができます。

Promise.all が、渡した配列の中に 1 つでもリジェクトされるものがあった場合にエラーを返すのに対し、Promise.any はリジェクトが発生しても、リゾルブしたものが 1 つでもあればそれを返します。

Promise.any に渡した配列に含まれる すべての Promise がリジェクトされた場合、結果として得られるエラーは AggregatedError です。以下のようなコード例を考えてみましょう:

const promise1 = Promise.reject(Error("No good, sorry!"));
const promise2 = Promise.reject(Error("Bad day ..."));

Promise.any([promise1, promise2])
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log("Always runs!"));

catch を使ってエラーを処理しています。このコードの実行結果は以下の通りです:

AggregateError: No Promise in Promise.any was resolved
Always runs!

AggregatedError オブジェクトは、通常の Error オブジェクトと同様のプロパティに加えて、errors プロパティを持っています:

//
  .catch(error => console.error(error.errors))
//

このプロパティは、それぞれのリジェクトで返されたエラーの配列を格納しています:

[Error: "No good, sorry!, Error: "Bad day ..."]

Promise.race のエラー処理

Promise.race は、Promsie の配列を引数に取ります:

const promise1 = Promise.resolve("The first!");
const promise2 = Promise.resolve("The second!");

Promise.race([promise1, promise2]).then(result => console.log(result));

// The first!

得られる返り値は、 「レース」を制した 1 番着の Promise です。

リジェクトされた場合はどうなるのでしょうか?リジェクトされる Promise が一番でなければ、Promise.race はリゾルブします:

const promise1 = Promise.resolve("The first!");
const rejection = Promise.reject(Error("Ouch!"));
const promise2 = Promise.resolve("The second!");

Promise.race([promise1, rejection, promise2]).then(result =>
  console.log(result)
);

// The first!

もし リジェクトが一番になった場合、Promise.race はリジェクトされ 、以下のようにしてリジェクトをキャッチすることができます:

const promise1 = Promise.resolve("The first!");
const rejection = Promise.reject(Error("Ouch!"));
const promise2 = Promise.resolve("The second!");

Promise.race([rejection, promise1, promise2])
  .then(result => console.log(result))
  .catch(error => console.error(error.message));

// Ouch!

Promise.allSettled のエラー処理

Promise.allSettled は ECMAScript 2020 で追加される関数です。

この関数を使って処理するケースはそれほど多くありません。なぜなら、 Promise のリジェクトがあったとしても、返り値は常にリゾルブされた Promise になるため です。

以下のようなコード例を考えてみます:

const promise1 = Promise.resolve("Good!");
const promise2 = Promise.reject(Error("No good, sorry!"));

Promise.allSettled([promise1, promise2])
  .then(results => console.log(results))
  .catch(error => console.error(error))
  .finally(() => console.log("Always runs!"));

上の例では、リゾルブする Promise とリジェクトされる Promise を 1 つずつ含め、配列として渡しています。

このケースでは、catch が実行されることはありません。代わりに finally が実行されます。

then 内の処理によってロギングされる結果は次の通りです:

[
  { status: 'fulfilled', value: 'Good!' },
  {
    status: 'rejected',
    reason: Error: No good, sorry!
  }
]

async/await のエラー処理

JavaScript の async/await は非同期関数を表しますが、コードを読む立場からみれば、同期関数の 可読性の高さ の恩恵を受けているといえます。

話を単純にするために、何度も登場している同期関数 toUppercase を、function キーワードの前に async を付け足すことで、非同期関数に変換します:

async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

async というプレフィックスを使うことで、関数に Promise を返す ように仕向けることができるようになります。これはつまり、thencatchfinally といったチェーンが使えることを意味します:

async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

toUppercase("abc")
  .then(result => console.log(result))
  .catch(error => console.error(error.message))
  .finally(() => console.log("Always runs!"));

async 関数内で例外を投げた 場合、この例外は、 裏側で機能している Promise をリジェクト させます。

どんなエラーも、catch によってキャッチすることができます。

最も重要なことは、このスタイルに加えて同期関数と同様に try/catch/finally 構文を使える、ということです。

以下の例では、toUppercase 関数を consumer という他の関数から呼び出しています。consumer 内部では、toUppercase 関数を try/catch/finally 関数で囲っています:

async function toUppercase(string) {
  if (typeof string !== "string") {
    throw TypeError("Wrong type given, expected a string");
  }

  return string.toUpperCase();
}

async function consumer() {
  try {
    await toUppercase(98);
  } catch (error) {
    console.error(error.message);
  } finally {
    console.log("Always runs!");
  }
}

consumer(); // Returning Promise ignored

実行結果は以下の通りです:

Wrong type given, expected a string
Always runs!

同様のトピックは次の記事でも扱っています: How to Throw Errors From Async Functions in JavaScript?

非同期ジェネレーターのエラー処理

JavaScript の 非同期ジェネレーター は、通常の値の代わりに Promise を yeild することができるジェネレーター関数 です。

async とジェネレーター関数を組み合わせて使います。イテレータオブジェクトが呼び出し元に対して Promise を返すジェネレーター関数です。

非同期ジェネレーターを作るために、async でプレフィックスした、* を持つ関数を定義します:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}

Promise の仕組みに基づいているため、エラー処理に対しても同様のルールが適用されます。非同期ジェネレーター関数内の throw は Promise のリジェクトに繋がり、catch でキャッチすることができます。

非同期ジェネレーター関数から Promise を取り出す には、以下の 2 つのアプローチがあります。

  • thenハンドラ
  • 非同期イテレーション

上のコード例では、最初の 2 つの値が yield されたあとに、例外が投げられます。これは以下のようにできることを意味します:

const go = asyncGenerator();

go.next().then(value => console.log(value));
go.next().then(value => console.log(value));
go.next().catch(reason => console.error(reason.message));

上記コードの実行結果は以下の通りです:

{ value: 33, done: false }
{ value: 99, done: false }
Something went wrong!

もう1つのアプローチは、 for await...of非同期イテレーション を用いる方法です。非同期イテレーションを用いるためには、呼び出し側の関数を async で囲む必要があります。

以下が完全なコード例です:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}

async function consumer() {
  for await (const value of asyncGenerator()) {
    console.log(value);
  }
}

consumer();

async/await で見たように、潜在的に存在する例外は try/catch で処理することができます:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  throw Error("Something went wrong!"); // Promise.reject
}

async function consumer() {
  try {
    for await (const value of asyncGenerator()) {
      console.log(value);
    }
  } catch (error) {
    console.error(error.message);
  }
}

consumer();

実行結果は以下の通りです:

33
99
Something went wrong!

非同期ジェネレーター関数の返り値であるイテレータオブジェクトには、同期ジェネレーター関数と同様に throw() メソッドがあります。

イテレータオブジェクトにおいて throw() メソッドを呼び出すと、例外は投げず、代わりにリジェクトされた Promise を投げます:

async function* asyncGenerator() {
  yield 33;
  yield 99;
  yield 11;
}

const go = asyncGenerator();

go.next().then(value => console.log(value));
go.next().then(value => console.log(value));

go.throw(Error("Let's reject!"));

go.next().then(value => console.log(value)); // value is undefined

この状況を処理するには、以下のようにできます:

go.throw(Error("Let's reject!")).catch(reason => console.error(reason.message));

ただし、イテレータオブジェクトの throw()ジェネレーター関数の内部 に例外を送ることを忘れないでおきましょう。これは、以下のようなパターンを適用することを意味します:

async function* asyncGenerator() {
  try {
    yield 33;
    yield 99;
    yield 11;
  } catch (error) {
    console.error(error.message);
  }
}

const go = asyncGenerator();

go.next().then(value => console.log(value));
go.next().then(value => console.log(value));

go.throw(Error("Let's reject!"));

go.next().then(value => console.log(value)); // value is undefined

Node.js のエラー処理

Node.js の同期エラー処理

Node.js における同期エラー処理は、今までみてきた内容とほとんど同じです。

同期コード には、try/catch/finally が使えます。

しかしながら、非同期の世界に目を向けてみると、面白いことが起こります。

Node.js の非同期エラー処理: コールバックパターン

非同期コードにおいては、Node.js は 2 つの書き方に依存しています:

  • コールバックパターン
  • イベントエミッター

コールバックパターン において 非同期 Node.js API は、 イベントループ を通して処理され コールスタック が空になるとすぐに実行されるという関数を引数に取ります。

以下のようなコードを考えてみましょう:

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) console.error(error);
    // do stuff with the data
  });
}

上のコードからコールバック関数を抽出すると、どのようにエラーを処理することになっているかを見ることができます:

//
function(error, data) {
    if (error) console.error(error);
    // do stuff with the data
  }
//

fs.readFile の実行過程においてエラーが発生した場合には、エラーオブジェクトを得ます。

この時点で、以下のことが可能です:

  • 今までしてきたように、単純にエラーオブジェクトのログを表示する
  • 例外を投げる
  • 他のコールバックにエラーを渡す

例外を投げる場合は、以下のようにできます:

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) throw Error(error.message);
    // do stuff with the data
  });
}

しかし、DOM におけるイベントやタイマーと同様に、この例外は プログラムをクラッシュ させます。以下のように try/catch を用いてクラッシュを阻止しようとしても、うまくいきません:

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) throw Error(error.message);
    // do stuff with the data
  });
}

try {
  readDataset("not-here.txt");
} catch (error) {
  console.error(error.message);
}

プログラムをクラッシュさせたくなければ、他のコールバックにエラーを渡すことが望ましい方法です。

const { readFile } = require("fs");

function readDataset(path) {
  readFile(path, { encoding: "utf8" }, function(error, data) {
    if (error) return errorHandler(error);
    // do stuff with the data
  });
}

ここで用いている eventHandler はその名前からも分かるように、エラーを処理するシンプルな関数です:

function errorHandler(error) {
  console.error(error.message);
  // do something with the error:
  // - write to a log.
  // - send to an external logger.
}

Node.js における非同期エラー処理: イベントエミッター

Node.js で行う多くのことは、 イベント に基づいています。ほとんどの場合、 エミッターオブジェクト と、いくつかのメッセージを待ち受けているオブザーバーとやり取りを行います。

Node.js のイベント駆動なモジュール(例えば net など)はすべて EventEmitter というルートクラスを継承しています。

Node.js の EventEmitter は、2 つの基本的なメソッド持っています: onemit です。

以下のような単純な HTTP サーバーを考えてみましょう:

const net = require("net");

const server = net.createServer().listen(8081, "127.0.0.1");

server.on("listening", function () {
  console.log("Server listening!");
});

server.on("connection", function (socket) {
  console.log("Client connected!");
  socket.end("Hello client!");
});

ここで、以下の 2 つのイベントを待ち受けています: listeningconnection です。

これらのイベントに加えて、イベントエミッターはエラーが発生した際に エラー イベントも発火します。

もしこの上記コードのポート番号を 80 にして実行した場合、以下のような例外を得るでしょう:

const net = require("net");

const server = net.createServer().listen(80, "127.0.0.1");

server.on("listening", function () {
  console.log("Server listening!");
});

server.on("connection", function (socket) {
  console.log("Client connected!");
  socket.end("Hello client!");
});

実行結果:

events.js:291
      throw er; // Unhandled 'error' event
      ^

Error: listen EACCES: permission denied 127.0.0.1:80
Emitted 'error' event on Server instance at: ...

この例外をキャッチするためには、 エラー を待ち受けるイベントハンドラを登録します:

server.on("error", function(error) {
  console.error(error.message);
});

以下のような結果を得ます:

listen EACCES: permission denied 127.0.0.1:80

さらに、プログラムはクラッシュしません。

このトピックについての詳細は、"Error Handling in Node.js" を読むと良いでしょう。

まとめ

このガイドでは、シンプルな同期コードから高度な非同期な仕組みまで、JavaScript のエラー処理全般を扱いました。

JavaScript のプログラムでは、例外の発生の仕方は多岐にわたります。

同期コードの例外は最も単純に対処することができますが、 非同期コード における例外処理は 複雑になる 場合があります。

一方で、ブラウザの新しい JavaScript API はほとんどすべて Promise に向かっています。普及したこのパターンは、then/catch/finally または async/awaittry/catch を使って例外を処理することをより簡単にします。

このガイドを読んだ後は、 プログラムで起こり得るすべての状況を認識して、例外を正しくキャッチすることができる ようになっているはずです。

最後までお読み頂きありがとうございました!

Discussion