1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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

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のインスタンスを引数に渡す

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のプロセスが落ちることはありません。

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()が用意されています。

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になった時実行するコールバックを登録するメソッドです。

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

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

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が出力されなくなります。

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

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

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

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

catch()

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

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

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

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

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

finally()

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

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

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

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

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

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インスタンスが解決されるまで解決されません。

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になります。

// 正常系
const allResolved = Promise.all([
	1,
	Promise.resolve('foo'),
	Promise.resolve(true)
])
setTimeout(() => console.log(allResolved), 1000)
>>>
Promise { [ 1, 'foo', true ] }
// 異常系
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()により並行実行する方が処理が早くなります。

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インスタンスが1つでもsettledになると、その他のPromiseインスタンスの結果を待たずにそのPromiseインスタンスと同じ状態になります。

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インスタンスを返します。

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

Promise.allSettled()

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

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 }
  ]
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?