Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8829

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

$
0
0

image.png

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

TOC

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

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

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

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

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

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

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

JavaScript におけるエラーとは?

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

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

consterr=newError("Something bad happened!");

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

consterr=Error("Something bad happened!");

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

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

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

constwrongType=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に再代入しようとした際に発生します:

constname="Jules";name="Caty";// TypeError: Assignment to constant variable.

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

vax='33';// SyntaxError: Unexpected identifier

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

functionwrong(){await99;}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とエラーオブジェクトを用います:

constwrongType=TypeError("Wrong type given, expected number");throwwrongType;

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

throwTypeError("Wrong type given, expected number");

または

thrownewTypeError("Wrong type given, expected number");

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

functiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.toUpperCase();}

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

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

throwSymbol();throw33;throw"Error!";thrownull;

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

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

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

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

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

functiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.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 の同期処理と非同期処理における、エラーと例外処理に話を進めましょう。

同期的エラー処理

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

通常関数のエラー処理

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

functiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.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(){yield33;yield99;}

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

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

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

function*generate(){yield33;yield99;}constgo=generate();

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

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

function*generate(){yield33;yield99;}constgo=generate();constfirstStep=go.next().value;// 33constsecondStep=go.next().value;// 99

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

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

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

function*generate(){yield33;yield99;}constgo=generate();constfirstStep=go.next().value;// 33go.throw(Error("Tired of iterating!"));constsecondStep=go.next().value;// never reached

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

function*generate(){try{yield33;yield99;}catch(error){console.error(error.message);}}

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

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

function*generate(){yield33;yield99;throwError("Tired of iterating!");}try{for(constvalueofgenerate()){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構文 で囲みたくなるかもしれません。

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

functionfailAfterOneSecond(){setTimeout(()=>{throwError("Something went wrong!");},1000)}

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

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

functionfailAfterOneSecond(){setTimeout(()=>{throwError("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 における仕組みと同様です。

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

constbutton=document.querySelector("button");button.addEventListener("click",function(){throwError("Can't touch this button!");});

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

constbutton=document.querySelector("button");try{button.addEventListener("click",function(){throwError("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><imgsrc="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 では、このエラーを以下のように「キャッチ」できるかもしれません:

constimage=document.querySelector("img");image.onerror=function(event){console.log(event);};

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

constimage=document.querySelector("img");image.addEventListener("error",function(event){console.log(event);});

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

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

Promise を用いたエラー処理

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

functiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.toUpperCase();}toUppercase(4);

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

functiontoUppercase(string){if(typeofstring!=="string"){returnPromise.reject(TypeError("Wrong type given, expected a string"));}constresult=string.toUpperCase();returnPromise.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(typeofvalue==="string"){throwTypeError("Expected a number!");}});

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

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

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

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

fetch("https://example-dev/api/").then(response=>{if(!response.ok){throwError(response.statusText);}returnresponse.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.

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

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

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

functionfailAfterOneSecond(){setTimeout(()=>{throwError("Something went wrong!");},1000);}// DOES NOT WORKtry{failAfterOneSecond();}catch(error){console.error(error.message);}

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

functionfailAfterOneSecond(){returnnewPromise((_,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 の配列を返します。

constpromise1=Promise.resolve("All good!");constpromise2=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が利用できます:

constpromise1=Promise.resolve("All good!");constpromise2=Promise.reject(Error("No good, sorry!"));constpromise3=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です。以下のようなコード例を考えてみましょう:

constpromise1=Promise.reject(Error("No good, sorry!"));constpromise2=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 の配列を引数に取ります:

constpromise1=Promise.resolve("The first!");constpromise2=Promise.resolve("The second!");Promise.race([promise1,promise2]).then(result=>console.log(result));// The first!

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

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

constpromise1=Promise.resolve("The first!");constrejection=Promise.reject(Error("Ouch!"));constpromise2=Promise.resolve("The second!");Promise.race([promise1,rejection,promise2]).then(result=>console.log(result));// The first!

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

constpromise1=Promise.resolve("The first!");constrejection=Promise.reject(Error("Ouch!"));constpromise2=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 になるためです。

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

constpromise1=Promise.resolve("Good!");constpromise2=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:Nogood,sorry!}]

async/await のエラー処理

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

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

asyncfunctiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.toUpperCase();}

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

asyncfunctiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.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関数で囲っています:

asyncfunctiontoUppercase(string){if(typeofstring!=="string"){throwTypeError("Wrong type given, expected a string");}returnstring.toUpperCase();}asyncfunctionconsumer(){try{awaittoUppercase(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でプレフィックスした、*を持つ関数を定義します:

asyncfunction*asyncGenerator(){yield33;yield99;throwError("Something went wrong!");// Promise.reject}

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

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

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

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

constgo=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で囲む必要があります。

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

asyncfunction*asyncGenerator(){yield33;yield99;throwError("Something went wrong!");// Promise.reject}asyncfunctionconsumer(){forawait(constvalueofasyncGenerator()){console.log(value);}}consumer();

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

asyncfunction*asyncGenerator(){yield33;yield99;throwError("Something went wrong!");// Promise.reject}asyncfunctionconsumer(){try{forawait(constvalueofasyncGenerator()){console.log(value);}}catch(error){console.error(error.message);}}consumer();

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

3399Somethingwentwrong!

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

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

asyncfunction*asyncGenerator(){yield33;yield99;yield11;}constgo=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()ジェネレーター関数の内部に例外を送ることを忘れないでおきましょう。これは、以下のようなパターンを適用することを意味します:

asyncfunction*asyncGenerator(){try{yield33;yield99;yield11;}catch(error){console.error(error.message);}}constgo=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");functionreadDataset(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");functionreadDataset(path){readFile(path,{encoding:"utf8"},function(error,data){if(error)throwError(error.message);// do stuff with the data});}

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

const{readFile}=require("fs");functionreadDataset(path){readFile(path,{encoding:"utf8"},function(error,data){if(error)throwError(error.message);// do stuff with the data});}try{readDataset("not-here.txt");}catch(error){console.error(error.message);}

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

const{readFile}=require("fs");functionreadDataset(path){readFile(path,{encoding:"utf8"},function(error,data){if(error)returnerrorHandler(error);// do stuff with the data});}

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

functionerrorHandler(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 サーバーを考えてみましょう:

constnet=require("net");constserver=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 にして実行した場合、以下のような例外を得るでしょう:

constnet=require("net");constserver=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を使って例外を処理することをより簡単にします。

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

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


Viewing all articles
Browse latest Browse all 8829

Trending Articles