-
-
Notifications
You must be signed in to change notification settings - Fork 224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(async): エラーファーストコールバック #509
Changes from 3 commits
56e13c5
a002f05
10f296e
d32cadf
ab7396e
30ed7bd
27f0e5a
2a3a4b6
6ac7951
8bd4249
8265257
03d32bb
35d1dfc
d5ce443
0445cff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -239,7 +239,108 @@ console.log("この文は実行されます"); | |
この章では主要な非同期処理と例外の扱い方としてエラーファーストコールバック、Promise、Async Functionの3つを見ていきます。 | ||
現実のコードではすべてのパターンが実用的です。そのため、非同期処理の選択肢を増やす意味でも理解することは重要です。 | ||
|
||
## エラーファーストコールバック {#error-first-callback} | ||
|
||
**非同期処理の中**で例外を発生した場合に、その例外を**非同期処理の外**へ伝える方法の1つとして**エラーファーストコールバック**があります。 | ||
エラーファーストコールバックとは、例外が発生したときはそエラーファーストコールバックのエラーをコールバック関数の最初の引数に入れて呼び出すという手法です。 | ||
このエラーファーストコールバックはNode.jsで好んで使われ、Node.jsの標準APIにおいても非同期処理を行う関数では利用されています。 | ||
|
||
たとえば、Node.jsでは`fs.readFile`関数というファイルシステムからファイルをロードする非同期処理を行う関数があります。 | ||
指定したパスのデータを読むため、ファイルが存在しない場合やアクセス権限の問題から読み取りに失敗することがあります。 | ||
そのため、`fs.readFile`関数の第2引数にわたすコールバック関数にはエラーファーストコールバックを指定します。 | ||
|
||
ファイルを読み込むことに失敗した場合には、 | ||
コールバック関数の1番目の引数にはErrorオブジェクトが渡されます。 | ||
ファイルを読み込むことに成功した場合には、コールバック関数の1番目の引数には`null`、2番目の引数に読み込んだデータを渡します。 | ||
|
||
```js | ||
fs.readFile("./example.txt", (error, data) => { | ||
if (error) { | ||
// 読み込み中にエラーが発生しました | ||
} else { | ||
// データを読み込むことができた | ||
} | ||
}); | ||
``` | ||
|
||
実際にエラーファーストコールバック関数を扱う処理を作りながら見ていきましょう。 | ||
|
||
次のコードの`callTaskAsync`関数は、第1引数に非同期的に呼び出すタスクとなる関数を受け取り、第2引数にエラーファーストコールバック関数を受け取ります。 | ||
第1引数のタスクとなる関数が失敗(例外を投げた)場合には、第2引数にエラーファーストコールバック関数にはエラーオブジェクトを渡して呼び出します。 | ||
一方、タスクとなる関数が成功(例外を投げなかった)場合には、第2引数にエラーファーストコールバック関数には`null`とそのタスクの返り値を渡して呼び出します。 | ||
|
||
```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 successTask = () => { | ||
return "タスクが成功しました"; | ||
}; | ||
const failtureTask = () => { | ||
throw new Error("タスクが失敗しました"); | ||
}; | ||
// sucessTaskは成功するため、`error`は`null`となり、`result`に値が入る | ||
callTaskAsync(successTask, (error, result) => { | ||
if (error) { | ||
console.log(error); // 呼ばれない | ||
} else { | ||
console.log(result); // => "タスクが成功しました" | ||
} | ||
}); | ||
// failtureTaskは失敗するため、`error`にはErrorオブジェクトが入る | ||
callTaskAsync(successTask, (error, result) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. このサンプルやっぱり長いなー。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/successTask/failtureTask/では・・・? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 直します |
||
if (error) { | ||
console.log(error); // => Error: タスクが失敗しました | ||
} else { | ||
console.log(result); // 呼ばれない | ||
} | ||
}); | ||
``` | ||
|
||
このように最初の引数にはエラーオブジェクトまたは`null`を入れ、それ以降の引数にデータを入れるというルール化したものをエラーファーストコールバック関数と呼びます。Node.jsでは標準APIの非同期処理においてエラーファーストコールバック関数が採用されています。 | ||
詳しい扱い方については[ユースケース: Node.jsでCLIアプリケーション][]について紹介します。 | ||
|
||
コールバック関数でエラー結果を受け取る方法は他にもやり方があります。 | ||
たとえば、成功したときに呼び出すコールバック関数と失敗したときに呼び出すコールバック関数の2つを受け取る方法があります。 | ||
さきほどの`callTaskAsync`をその形に変更すると次のような実装になります。 | ||
|
||
```js | ||
/** | ||
* `task`を実行して、成功なら`successCallback(タスクの返り値)`と呼び出す | ||
* 失敗なら`failureCallback(error)`と呼び出す | ||
*/ | ||
function callTaskAsync(task, successCallback, failureCallback) { | ||
setTimeout(() => { | ||
try { | ||
const result = task(); | ||
successCallback(result); | ||
} catch (error) { | ||
failureCallback(error); | ||
} | ||
}, 10); | ||
} | ||
``` | ||
|
||
このように、**非同期処理の中**で例外を発生した場合に、その例外を**非同期処理の外**へ伝える方法はさまざまな手段が考えられます。 | ||
エラーファーストコールバックはその形を決めた**ただの共通のルール**です。そのため、必ずしもこのパターンがすべてにおいて正しいわけではありません。 | ||
一方で、非同期処理における例外処理のパターンを決めることのメリットとして、エラーハンドリングの共通化や書きやすさなどがあります。 | ||
|
||
次のセクションでは、エラーファーストコールバックでは**ただの共通のルール**であったエラーハンドリングを、**統一的なインターフェース**として扱えるようにした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/node-cli/README.md |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); // タスクが成功した場合 | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
これ一回result変数で受けているのはなんででしょう・・・?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
特別な理由はないですが、
result
という変数名で意味を説明したいのとインラインで書くと評価順が異なるため結果は同じですがやりたいことが異なるためですね。このケースでは結果が同じになりますが、もし左辺に副作用があるものが存在していた場合に結果が異なる場合がありますね。(
await
を使うと実際そういうミスをしやすい。副作用があるかどうで書き方を変えるよりは、同じ書き方を取れる方を優先したいですね)やりたいことは
task()
の返り値をcallback
に渡したいのであって、callback
を呼ぶためにtask()
を実行するではないからですね。(いいたいこととしては、「まずtask()
を評価する」ことが先にある点ですね)短くかけますが余計なミスが発生する可能性もあるので、一時変数
result
を作ったほうが誰が読んでも同じに読めると思えるからですね。