[Node.js] 非同期処理 - Promise編

ES2015で導入された非同期処理の状態と結果を表現するオブジェクトです。
Promiseを利用した非同期関数の実装では、関数は呼び出されるとその場ですぐPromiseインスタンスを返す必要がありますが、処理の結果を確定するのはあとから(非同期)です。結果が未確定のPromiseインスタンスの状態を<b>pending</b>といい、非同期処理に成功した状態を<b>fulfilled</b>と呼ぶ。一度でも"fulfilled"または"rejected"になったPromiseインスタンスの状態はそれ以降変化せず、これらの状態を<b>settled</b>と総称します。

~~~javascript
function parseJSONAsync(json) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			try {
				resolve(JSON.parse(json))
			} catch (err) {
				reject(err)
			}
		}, 1000)
	})
}
~~~
Promiseのコンストラクタを使ってPromiseインスタンスを作っています。Promiseのコンストラクタは関数をぱらめーたとし、この関数はresolve, rejectという2つの関数を引数として実行されます。コンストラクタが実行された時点ではこのPromiseインスタンスの状態は`pending`です。処理結果を引数にresolve()を実行すると`fulfilled`になり、エラーを引数にreject()を実行すると、"rejected"になります。
※ resuleve()の引数は必須ではなく、引数なしで実行するとからの値を持ったPromiseになる
※ reject()の引数も同様に非必須で、かつ任意の値を利用できるが、通常はErrorのインスタンスを引数に渡す

~~~javascript
const toBeFulfilled = parseJSONAsync('{"foo": 1}');
const toBeRejected = parseJSONAsync('不正なJSON');
console.log('****** Promise生成直後 ******');
console.log(toBeFulfilled);
console.log(toBeRejected);
setTimeout(() => {
	console.log('******* 1秒後 *******');
	console.log(toBeFulfilled);
	console.log(toBeRejected);
}, 1000)
>>>
****** Promise生成直後 ******
Promise { <pending> }
Promise { <pending> }
(node:9042) UnhandledPromiseRejectionWarning: SyntaxError: Unexpected token 不 in JSON at position 0
	at JSON.parse (<anonymous>)
	... (省略)
******* 1秒後 *******
Promise { { foo: 1 } }
Promise {
	<rejected> SyntaxError: Unexpected token 不 in JSON at position 0
      at JSON.parse (<anonymous>)
	... (省略)
}
~~~
生成した2つのPromiseインスタンスの状態は最初どちらも`pending`です。1秒経過すると正常なJSONを渡した方はそのパース結果を保持した`fulfilled状態`となり、不正なJSONを渡した方は`rejected状態`になります。

Promiseインスタンスの状態以外に注目すべき点は、`UnhandledPromiseRejectionWarning`が出力されています。これは`rejected状態`になったPromiseインスタンスに対して、イベントループが次のフェーズに進むまでエラーハンドリングが行われなかった場合に出力される警告です。この時、processオブジェクトが`UnhandledRejection`イベントを発行します。`UncaughtException`と異なり、このイベントはREPLでも発行され、デフォルトではこのイベントが発行される状況でもNode.jsのプロセスが落ちることはありません。

~~~javascript
process.on('UnhandledRejection', (err, promise) => 
  console.log('UnhandledRejection発生', err));
~~~
`UnhandledPromiseRejectionWarning`で述べられているとおり、`UnhandledRejection`が発行されている状況でNode.jsのプロセスを終了させたい場合は、nodeコマンド実行時に、`--unhandled-rejections=strict`を指定します。
そのほかに`process.exit(1);`などを使ってプロセスをエラー終了する方法もあります。

pendingを経ず、`fulfilled`または`rejected`なPromiseインスタンスを直接生成したい場合、Promiseのコンストラクタも使えるが、より簡単な手段として、`Promise.resolve()`, `Promise.reject()`が用意されています。

~~~javascript
const result1 = new Promise(resolve => resolve({ foo: 1 }))
const result2 = Promise.resolve({foo: 1})
const result3 = new Promise((resolve, reject) => reject(new Error('エラー')))
const result4 = Promise.reject(new Error('エラー'))

console.log(result1)
console.log(result2)
console.log(result3)
console.log(result4)
>>>
Promise { { foo: 1 } }
Promise { { foo: 1 } }
Promise {
  <rejected> Error: エラー
  ... (省略)
}
Promise {
  <rejected> Error: エラー
  ... (省略)
}
~~~

### then(), catch(), finally()

#### then()
`then()`はPromiseインスタンスの状態がfulfilledまたはrejectedになった時実行するコールバックを登録するメソッドです。

~~~javascript
promise.then(
	value => {
		// 成功時の処理
	},
	err => {
		// 失敗時の処理
	}
)
~~~

onFulfilledの引数には解決済みのPromiseインスタンスの値が、onRejectedの引数には拒否理由が渡されます。then()の戻り値は、登録したコールバックの戻り値で解決される新しいPromiseインスタンスです。また、then()の実行は元のPromiseインスタンスには影響を及ぼしません。
元のPromiseインスタンスが何らかの理由で拒否された時、onRejectedを省略するとthen()の戻り値のPromiseインスタンスも同じ理由で拒否されます。

~~~javascript
const stringPromise = Promise.resolve('{"foo": 1}');
console.log(stringPromise);
const numberPromise = stringPromise.then(str => str.length);

const unrecoveredPromise = Promise
  .reject(new Error('エラー'))
  .then(() => 1)

setTimeout(() => {
	console.log(numberPromise);
	console.log(unrecoveredPromise)
}, 1000)
>>>
Promise { '{"foo": 1}' }
(node:9334) UnhandledPromiseRejectionWarning: Error: エラー
... (省略)
Promise { 10 }
Promise {
  <rejected> Error: エラー
  ... (省略)
~~~

一方、onRejectedを省略せずに何か値を返すようにするとその値で解決されたPromiseインスタンスが得られます。この結果`UnhandledPromiseRejectionWarning`が出力されなくなります。

~~~javascript
const unrecoveredPromise = Promise
	.reject(new Error('エラー'))
	.then(() => 1, err => err.message)

setTimeout(() => console.log(unrecoveredPromise), 1000)
>>>
Promise { 'エラー' }
~~~

then()によるPromiseのチェーンで非同期処理の逐次実行を容易に実装できます。

~~~javascript
asyncFunc1(input)
  .then(asyncFunc2)
  .then(asyncFunc3)
  .catch(err => {
	  // エラーハンドリング
  })
~~~

#### catch()
catch()も非同期処理の結果をハンドリングするためのインスタンスメソッドの1つです、then()の引数に渡す`onFulfilled, onRejected`はどちらも省略可能で、then()の代わりにcatch()を使用できます。

~~~javascript
const catchedPromise = Promise.reject(new Error('エラー')).catch(() => 0)
setTimeout(() => console.log(catchedPromise), 1000)
>>>
Promise { 0 }
~~~

Promiseチェーンの最後にcatch()を記述すれば、エラーハンドリングを1箇所に集約できます。

~~~javascript
asyncFunc1(input)
.then(
	asyncFunc2,
	err => {
		// エラーハンドリング
	}
)
.then(
	result => {
		// 処理
	},
	err => {
		// asyncFunc2用のエラーハンドリング
	}
)

asyncFunc1(input)
.then(asyncFunc2)
.then(result => {
	// 処理
})
.catch(err => {
	// エラーハンドリングを集約できる
})
~~~

#### finally()
try...catch構文におけるfinallyブロック相当の機能を提供している。すなわち、非同期処理が成功したかどうかに関わらず、Promiseインスタンスが`settled状態`になった時実行されるコールバックを登録できます。

~~~javascript
const onFinally = Promise.resolve().finally(() => console.log('finallyのコールバック'));
console.log(onFinally);
>>>
Promise { <pending> }
finallyのコールバック
~~~

catch()と異なり、finally()のコールバックの戻り値はPromiseインスタンスが解決される値に影響しません。

~~~javascript
const returnValueInFinally = Promise.resolve(1).finally(() => 2)
setTimeout(() => console.log(returnValueInFinally), 1000)
>>>
Promise { 1 }
~~~

異常系ではコールバックの挙動が返されるPromiseインスタンスに影響します。
コールバックないでエラーがthrowされる場合や、コールバックがrejectedなPromiseインスタンスを返す場合、finally()の返すPromiseインスタンスも同じ理由で拒否されます。

~~~javascript
const throwErrorInFinally = Promise.resolve(1).finally(() => { throw new Error('エラー') });
setTimeout(() => console.log(throwErrorInFinally), 1000)
>>>
(node:9673) UnhandledPromiseRejectionWarning: Error: エラー
... (省略)
Promise {
  <rejected> Error: エラー
  ... (省略)
~~~

コールバックの戻り値がPromiseインスタンスの場合、finally()の返すPromiseインスタンスはコールバックの返すPromiseインスタンスが解決されるまで解決されません。

~~~javascript
Promise
	.resolve('foo')
	.finally(() => 
		new Promise(resolve => 
			setTimeout(() => {
				console.log('finally() 1秒経過')
				resolve()
			}, 1000)
		)
	)
	.then(console.log)
>>>
finally() 1秒経過
foo
~~~

### Promiseのスタティックメソッドを自用した並行実行
#### Promise.all()
Promise.all()の返すPromiseインスタンスは、引数に含まれるPromiseインスタンスが全てfulfilledになった時fulfilledになり、1つでもrejectedになるとその他のPromiseインスタンスの結果を待たずにrejectedになります。

~~~js
// 正常系
const allResolved = Promise.all([
	1,
	Promise.resolve('foo'),
	Promise.resolve(true)
])
setTimeout(() => console.log(allResolved), 1000)
>>>
Promise { [ 1, 'foo', true ] }
~~~

~~~js
// 異常系
const containRejected = Promise.all([
	1,
	Promise.reject('foo'),
	Promise.resolve(true)
])
setTimeout(() => console.log(containRejected), 1000)
>>>
(node:9936) UnhandledPromiseRejectionWarning: foo
... (省略)
Promise { <rejected> 'foo' }
~~~

複数の非同期処理を逐次実行する必要がなければ、Promise.all()により並行実行する方が処理が早くなります。

~~~js
import perf_hooks from 'perf_hooks';

const asyncFunc = () => new Promise(resolve => setTimeout(resolve, 1000))
const start = perf_hooks.performance.now();

asyncFunc()
	.then(asyncFunc)
	.then(asyncFunc)
	.then(asyncFunc)
	.then(() => console.log('逐次実行所要時間', perf_hooks.performance.now() - start))
	

Promise.all([
	asyncFunc(),
	asyncFunc(),
	asyncFunc(),
	asyncFunc()
])
.then(() => console.log('並行実行所要時間', perf_hooks.performance.now() -start))
>>>
並行実行所要時間 1002.2058520019054
逐次実行所要時間 4016.1091419905424
~~~

#### Promise.race()
Promise.race()の返すPromiseインスタンスは、引数に含まれるPromiseインスタンスが１つでも`settled`になると、その他のPromiseインスタンスの結果を待たずにそのPromiseインスタンスと同じ状態になります。

~~~js
const wait = (time) => new Promise(resolve => setTimeout(resolve, time))

const fulfilledFirst = Promise.race([
	wait(10).then(() => 1),
	wait(30).then(() => 'foo'),
	wait(20).then(() => Promise.reject(new Error('エラー')))
])

const rejectFirst = Promise.race([
	wait(20).then(() => 1),
	wait(30).then(() => 'foo'),
	wait(10).then(() => Promise.reject(new Error('エラー')))
])

const containNonPromise = Promise.race([
	wait(10).then(() => 1),
	'foo',
	wait(20).then(() => Promise.reject(new Error('エラー')))
])

setTimeout(() => console.log(fulfilledFirst), 1000)
setTimeout(() => console.log(rejectFirst), 1000)
setTimeout(() => console.log(containNonPromise), 1000)
>>>
Promise { 1 }
Promise {
  <rejected> Error: エラー
  ... (省略)
}
Promise { 'foo' }
~~~

引数にから配列を渡すと、解決されることのないPromiseインスタンスを返します。

~~~js
const raceWithEmptyArray = Promise.race([]);
setTimeout(() => console.log(raceWithEmptyArray), 1000);
>>>
Promise { <pending> }
~~~

#### Promise.allSettled()
Promise.allSettled()の返すPromiseインスタンスは、引数に含まれるPromiseインスタンスが全て`settled`になったとき`fulfilled`になります。

~~~js
const allSettled = Promise.allSettled([
	1,
	Promise.resolve('foo'),
	Promise.reject(new Error('エラー')),
	Promise.resolve(true)
])

setTimeout(() => console.log(allSettled), 1000
>>>Promise {
  [
    { status: 'fulfilled', value: 1 },
    { status: 'fulfilled', value: 'foo' },
    {
      status: 'rejected',
      reason: Error: エラー
    },
    { status: 'fulfilled', value: true }
  ]
}
~~~