diff --git a/prh.yml b/prh.yml index 6a6b138d81..4004b82b63 100644 --- a/prh.yml +++ b/prh.yml @@ -236,11 +236,12 @@ rules: - expected: HTML要素 patterns: - DOM要素 - - expected: 同期処理 + - expected: エラーファーストコールバック patterns: - - 動機処理 + - エラーファーストコールバック関数 + - # 一致とマッチ + # 一致とマッチ ## 一致は === - expected: $1一致 patterns: @@ -289,6 +290,9 @@ rules: - expected: されています patterns: - されいます + - expected: 同期処理 + patterns: + - 動機処理 # 文体 - expected: どのように diff --git a/source/basic/async/README.md b/source/basic/async/README.md index 8259ca0a80..7fb5b24835 100644 --- a/source/basic/async/README.md +++ b/source/basic/async/README.md @@ -239,7 +239,119 @@ console.log("この文は実行されます"); この章では主要な非同期処理と例外の扱い方としてエラーファーストコールバック、Promise、Async Functionの3つを見ていきます。 現実のコードではすべてのパターンが実用的です。そのため、非同期処理の選択肢を増やす意味でも理解することは重要です。 +## エラーファーストコールバック {#error-first-callback} + +ECMAScript 2015(ES2015)でPromiseが仕様へ入るまで、非同期処理中に発生した例外を扱う統一的な方法は存在しませんでした。 +ES2015より前までは、**エラーファーストコールバック**という非同期処理中に発生した例外を扱う方法を決めたコミュニティベースのルールが広く使われていました。 + +エラーファーストコールバックとは、次のような非同期処理におけるコールバック関数の呼び出し方を決めたルールです。 + +- 処理に失敗した場合は、コールバック関数の1番目の引数にエラーオブジェクトを渡して呼び出す +- 処理に成功した場合は、コールバック関数の1番目の引数には`null`を渡し、2番目以降の引数に成功時の結果などを渡して呼び出す + +つまり、ひとつのコールバック関数で失敗した場合と成功した場合の両方を扱うルールとなります。 + +たとえば、Node.jsでは`fs.readFile`関数というファイルシステムからファイルをロードする非同期処理を行う関数があります。 +指定したパスのデータを読むため、ファイルが存在しない場合やアクセス権限の問題から読み取りに失敗することがあります。 +そのため、`fs.readFile`関数の第2引数にわたすコールバック関数にはエラーファーストコールバックスタイルの関数を渡します。 + +ファイルを読み込むことに失敗した場合は、コールバック関数の1番目の引数には`Error`オブジェクトが渡されます。 +ファイルを読み込むことに成功した場合は、コールバック関数の1番目の引数には`null`、2番目の引数に読み込んだデータを渡します。 + + +```js +fs.readFile("./example.txt", (error, data) => { + if (error) { + // 読み込み中にエラーが発生しました + } else { + // データを読み込むことができた + } +}); +``` + +このエラーファーストコールバックはNode.jsでは広く使われ、Node.jsの標準APIにおいても非同期処理を行う関数では利用されています。 +詳しい扱い方については[ユースケース: Node.jsでCLIアプリケーション][]にて紹介します。 + +実際にエラーファーストコールバックで非同期な例外処理を扱うコードを書いてみましょう。 + +次のコードの`callTaskAsync`関数は、第1引数に非同期的に呼び出すタスクとなる関数を受け取り、第2引数にエラーファーストコールバックスタイルの関数を受け取ります。 +第1引数のタスクとなる関数が失敗(例外を投げた)場合には、第2引数のコールバック関数にはエラーオブジェクトを渡して呼び出します。 +一方、タスクとなる関数が成功(例外を投げなかった)場合には、第2引数のコールバック関数には`null`とそのタスクの返り値を渡して呼び出します。 + +{{book.console}} +```js +/** + * `task`を実行して、成功なら`callback(null, タスクの返り値)`と呼び出す + * 失敗なら`callback(error)`と呼び出す + */ +function callTaskAsync(task, callback) { + // タスクを非同期的に呼び出して、結果によってcallbackを呼び分ける + setTimeout(() => { + try { + const result = task(); + callback(null, result); + } catch (error) { + callback(error); + } + }, 10); +} +// 非同期処理が失敗する場合 +const failtureTask = () => { + throw new Error("タスクが失敗しました"); +}; +// failtureTaskは失敗するため、`error`にはErrorオブジェクトが入る +callTaskAsync(failtureTask, (error, result) => { + if (error) { + console.log(error); // => Error: タスクが失敗しました + } else { + console.log(result); // この文は実行されません + } +}); +// 非同期処理が成功する場合 +const successTask = () => { + return "タスクが成功しました"; +}; +// sucessTaskは成功するため、`error`は`null`となり、`result`に値が入る +callTaskAsync(successTask, (error, result) => { + if (error) { + console.log(error); // この文は実行されません + } else { + console.log(result); // => "タスクが成功しました" + } +}); +``` + +このようにコールバック関数の1番目の引数にはエラーオブジェクトまたは`null`を入れ、それ以降の引数にデータを渡すというルール化したものを**エラーファーストコールバック**と呼びます。 + +非同期処理中に例外が発生して生じたエラーをコールバック関数で受け取る方法は他にもやり方があります。 +たとえば、成功したときに呼び出すコールバック関数と失敗したときに呼び出すコールバック関数の2つを受け取る方法があります。 +さきほどの`callTaskAsync`を2種類のコールバック関数を受け取る形に変更すると次のような実装になります。 + +```js +/** + * `task`を実行して、成功なら`successCallback(タスクの返り値)`を呼び出す + * 失敗なら`failureCallback(error)`を呼び出す + */ +function callTaskAsync(task, successCallback, failureCallback) { + setTimeout(() => { + try { + const result = task(); + successCallback(result); + } catch (error) { + failureCallback(error); + } + }, 10); +} +``` + +このように**非同期処理の中**で例外が発生した場合に、その例外を**非同期処理の外**へ伝える方法はさまざまな手段が考えられます。 +エラーファーストコールバックはその形を決めた**ただの共通のルール**の1つです。そのため、エラーファーストコールバック以外の方法が使われていることも多いです。 +一方で、非同期処理における例外処理のルールを決めることのメリットとして、エラーハンドリングのパターン化ができることなどがあります。 + +エラーファーストコールバックは非同期処理におけるエラーハンドリングの**ただの共通のルール**でした。 +次のセクションでは、ES2015で導入されたPromiseという非同期処理を**統一的なインターフェース**として扱えるようにしたものを見ていきます。 [文と式]: ../statement-expression/README.md [例外処理]: ../error-try-catch/README.md [Web Worker]: https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Using_web_workers +[ユースケース: Node.jsでCLIアプリケーション]: ../../use-case/nodecli/README.md \ No newline at end of file diff --git a/source/basic/async/example/promise-catch.js b/source/basic/async/example/promise-catch.js index e69de29bb2..1f030478b8 100644 --- a/source/basic/async/example/promise-catch.js +++ b/source/basic/async/example/promise-catch.js @@ -0,0 +1,31 @@ +/** + * `delay * 1.5`ミリ秒以内にタイマーが呼ばれたら成功、呼ばれなかったら失敗とする関数 + * @param {Function} callback + * @param {number} delay タイマーのコールバックを呼び出すまでの時間(ミリ秒) + */ +const exactSetTimeout = (callback, delay) => { + return new Promise((resolve, reject) => { + // タイマーのコールバックが呼ばれるまでの許容時間(ミリ秒) + // `delay`に指定された時間の1.5倍まで許容する + const limitOfDelay = delay * 1.5; + const startTime = Date.now(); + setTimeout(() => { + const diffTime = Date.now() - startTime; + if (diffTime <= limitOfDelay) { + return resolve(); + } else { + return reject(new Error(`許容時間内にタイマーが呼ばれませんでした${diffTime}ミリ秒)`)); + } + }, delay); + }); +}; + +exactSetTimeout((error, message) => { + if (error) { + console.error(error); + return; + } + console.log(message); +}, 10); + + diff --git a/source/basic/async/example/try-catch.js b/source/basic/async/example/try-catch.js index dc43c4319f..ebac355e05 100644 --- a/source/basic/async/example/try-catch.js +++ b/source/basic/async/example/try-catch.js @@ -1,29 +1,59 @@ /** - * 指定時間内にタイマーが発火されるなら成功、そうでないなら失敗 - * @param callback + * `delay * 1.5`ミリ秒以内にタイマーが呼ばれたら成功、呼ばれなかったら失敗とする関数 + * @param {Function} callback + * @param {number} delay タイマーのコールバックを呼び出すまでの時間(ミリ秒) */ -const tryTimeout = (callback) => { - // タイマーのコールバックを呼び出すまでの時間(ミリ秒) - const delay = 10; - // タイマーのコールバックが呼ばれるまで待てる時間(ミリ秒) - const limitOfDelay = delay * 2; +const exactSetTimeout = (callback, delay) => { + // `delay`に指定された時間の1.5倍まで許容する + const limitOfDelay = delay * 1.5; const startTime = Date.now(); setTimeout(() => { const diffTime = Date.now() - startTime; if (diffTime <= limitOfDelay) { - callback(null, "許容時間内にタイマーが発火しました"); + callback(null, `許容時間内にタイマーが呼ばれました${diffTime}ミリ秒)`); } else { - callback(new Error(`許容時間よりタイマーが発火できませんでした(${diffTime}ミリ秒)`)); + callback(new Error(`許容時間内にタイマーが呼ばれませんでした${diffTime}ミリ秒)`)); } }, delay); }; -tryTimeout((error, message) => { +exactSetTimeout((error, message) => { if (error) { console.error(error); return; } console.log(message); -}); +}, 10); + + +/** + * `task`を実行して、成功なら、`callback(null, タスクの返り値)`と呼び出す + * 失敗なら、`callback(error)`と呼び出す + * @param {Function} task + * @param {(error: null|Error, result: *)} callback + */ +function callTaskAsync(task, callback) { + setTimeout(() => { + try { + const result = task(); + callback(null, result); + } catch (error) { + callback(error); + } + }, 10); +} +const successTask = () => { + return "成功!"; +}; +const failtureTask = () => { + throw new Error("タスクが失敗しました"); +}; +callTaskAsync(successTask, (error, result) => { + if (error) { + console.log(error); // タスクが失敗した場合 + } else { + console.log(result); // タスクが成功した場合 + } +});