Promise のメモ
記事にしました。
トピックの方は内容が古いままになっているので、記事の方を見ていただければと。
Promise の挙動をきちんと理解していなかったのでメモ。
なお、ここにメモしていることの大半が以下をきちんと理解していれば迷うことはなかった。
待機状態のプロミスは、何らかの値を持つ履行 (fulfilled) 状態、もしくは何らかの理由 (エラー) を持つ拒否 (rejected) 状態のいずれかに変わります。そのどちらとなっても、then メソッドによって関連付けられたハンドラーが呼び出されます。対応するハンドラーが割り当てられたとき、既にプロミスが履行または拒否状態になっていても、そのハンドラーは呼び出されます。よって、非同期処理とその関連付けられたハンドラーとの競合は発生しません。
とくに以下の部分。
対応するハンドラーが割り当てられたとき、既にプロミスが履行または拒否状態になっていても、そのハンドラーは呼び出されます。よって、非同期処理とその関連付けられたハンドラーとの競合は発生しません。
コードを動かしている環境
バージョンと設定など
$ node --version
v14.18.2
{
"name": "promise-memo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"directories": {
"example": "examples"
},
"scripts": {
"test": "node --loader ts-node/esm examples/memo/promise_is_pending.ts"
},
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^17.0.16",
"ts-node": "^10.5.0",
"typescript": "^4.5.5"
}
}
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"skipLibCheck": true,
"declaration": true,
"pretty": true,
"newLine": "lf",
"outDir": "dist",
"esModuleInterop": true
},
"exclude": ["node_modules", "src/**/*.test.ts"],
"include": [
"examples/**/*.ts",
"src/**/*.ts",
"test/**/*.spec.ts",
"src/**/*.tsx"
]
}
コールバックは(同期的に)即座に開始される
new Promise(cb)
を実行すれば then()
や await
を実行しなくとも指定した cb
は開始されている。
;(async () => {
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const p = new Promise<string>((resolve) => {
console.log('cb start')
setTimeout(() => resolve('done'), 1000)
})
console.log('step0')
await wait(2000)
console.log('step1')
await p.then((v) => console.log(v))
console.log('step2')
})()
$ node --loader ts-node/esm examples/memo/cb_call_immediately.ts
cb start
step0
step1
done
step2
then()
は新しい Promise(のインスタンス)を返す
ここを理解していると後のことがしっくりくる。
;(async () => {
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const p = new Promise<string>((resolve) => {
setTimeout(() => resolve('done'), 1000)
})
const t1 = p.then((v) => v)
const t2 = p.then((v) => v)
console.log(`t1 === t2 : ${t1 === t2}`)
console.log(`t1: ${await t1}`)
console.log(`t2: ${await t2}`)
})()
$ node --loader ts-node/esm examples/memo/then_return_new_promise.ts
t1 === t2 : false
t1: done
t2: done
then()
await
は同時に複数回利用できる
決定後も利用できるが、決定された値は変更できない。
;(async () => {
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
let r: (value: string) => void = (v) => {
console.log('called before settled')
}
const p = new Promise<string>((resolve) => {
console.log('cb start')
setTimeout(() => {
resolve('done')
r = resolve
}, 1000)
})
;(async () => {
p.then((v) => console.log(`then-1: ${v}`))
})()
;(async () => {
console.log(`await-1: ${await p}`)
})()
;(async () => {
p.then((v) => console.log(`then-2: ${v}`))
})()
;(async () => {
console.log(`await-2: ${await p}`)
})()
await p
console.log('another resolve()')
r('done-2')
;(async () => {
p.then((v) => console.log(`then-1: ${v}`))
})()
;(async () => {
console.log(`await-1: ${await p}`)
})()
;(async () => {
p.then((v) => console.log(`then-2: ${v}`))
})()
;(async () => {
console.log(`await-2: ${await p}`)
})()
})()
$ node --loader ts-node/esm examples/memo/then_await_call_multiple.ts
cb start
then-1: done
await-1: done
then-2: done
await-2: done
another resolve()
then-1: done
await-1: done
then-2: done
await-2: done
決定後に再利用しても chain を再実行するわけではない
新しいハンドラーは実行されるが、決定された状態は各インスタンスが保持しているもよう。
const p = new Promise((resolve) => setTimeout(() => resolve('p')))
const t1 = p.then((v) => {
console.log('t1')
return `${v}-t1`
})
const t2 = t1.then((v) => {
console.log('t2')
return `${v}-t2`
})
const t3 = t2.then((v) => {
console.log('t3')
return `${v}-t3`
})
console.log(await t3)
console.log(await t3)
const t4 = t3.then((v) => {
console.log('t4')
return `${v}-t4`
})
console.log(await t4)
console.log(await t2)
export {}
$ node --loader ts-node/esm examples/memo/chain-not-replay.ts
t1
t2
t3
p-t1-t2-t3
p-t1-t2-t3
t4
p-t1-t2-t3-t4
p-t1-t2
catch
も複数同時に利用できる
基本は then()
await
と同じ。
catch()
のコールバックが実行されるだけでなく、try-catch
でも throw される。
// catch は同時に複数回利用できる(throw される)。
// 決定後も利用できる(throw される)。
;(async () => {
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const p = new Promise<string>((resolve, reject) => {
console.log('cb start')
setTimeout(() => reject('rejected'), 1000)
})
console.log('before settled')
;(async () => {
p.catch((v) => console.log(`catch-1: ${v}`))
})()
;(async () => {
try {
await p
} catch (r) {
console.log(`await-1: ${r}`)
}
})()
;(async () => {
p.catch((v) => console.log(`catch-2: ${v}`))
})()
;(async () => {
try {
await p
} catch (r) {
console.log(`await-2: ${r}`)
}
})()
await p.catch((r) => r)
console.log('after settled')
;(async () => {
p.catch((v) => console.log(`catch-1: ${v}`))
})()
;(async () => {
try {
await p
} catch (r) {
console.log(`await-1: ${r}`)
}
})()
;(async () => {
p.catch((v) => console.log(`catch-2: ${v}`))
})()
;(async () => {
try {
await p
} catch (r) {
console.log(`await-2: ${r}`)
}
})()
})()
$ node --loader ts-node/esm examples/memo/catch_multiple.ts
cb start
before settled
catch-1: rejected
await-1: rejected
catch-2: rejected
await-2: rejected
after settled
catch-1: rejected
await-1: rejected
catch-2: rejected
await-2: rejected
reject の chatch は各 Promise(のインスタンスの chain) に 1 つ必要
返信に追記あり
await
などを行う毎に「新しい Promise が返ってくる」ので、その都度 catch
しないといけない。
そのため、new Promise()
に catch
を付けるだけでは reject
が漏れるときがある。
chain に 1 つあれば大丈夫だが、catch
を付けるまえに await
を行うと「新しい Promise が返ってくる = chain が分岐するので」漏れる可能性がある。
サンプルコードは返信に追記。
;(async () => {
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const p = new Promise<string>((resolve, reject) => {
console.log('cb start')
setTimeout(() => reject('rejected'), 1000)
})
console.log('before sttled')
;(async () => {
p.catch((v) => console.log(`catch-1: ${v}`))
})()
;(async () => {
try {
await p
} catch (r) {
console.log(`await-1: ${r}`)
}
})()
;(async () => {
p.catch((v) => console.log(`catch-2: ${v}`))
})()
;(async () => {
try {
await p
} catch (r) {
console.log(`await-2: ${r}`)
}
})()
await p.catch((r) => r)
console.log('after settled')
// console.log('then only')
// p.then((v) => v)
console.log('await only')
await p
})()
$ node --loader ts-node/esm examples/memo/each_await_need_catch.ts
cb start
before sttled
catch-1: rejected
await-1: rejected
catch-2: rejected
await-2: rejected
after settled
await only
(node:1774651) UnhandledPromiseRejectionWarning: rejected
(node:1774651) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:1774651) [DEP0018] 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.
;(async () => {
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const p = new Promise<string>((resolve, reject) => {
console.log('cb start')
setTimeout(() => reject('rejected'), 1000)
})
const tc1 = p.then((v) => {}).catch((r) => r)
const tc2 = p.then((v) => {})
;(async () => {
console.log('tc1: then catch -> await chain')
console.log(`tc1: ${await tc1}`)
})()
;(async () => {
console.log('tc2: then -> await chain')
console.log(`tc2: ${await tc2}`)
})()
})()
$ node --loader ts-node/esm examples/memo/each_chain_need_catch.ts
cb start
tc1: then catch -> await chain
tc2: then -> await chain
tc1: rejected
(node:2186001) UnhandledPromiseRejectionWarning: rejected
(node:2186001) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:2186001) [DEP0018] 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.
上記サンプルを再編した
Promise.race に同一の Promise を何度も渡すことができる
ここまでの挙動を見ていれば普通のことなのだが、「一度 Promise.race
を通したらハンドラーは動作しない」と勘違いしていた。
以下は、少しわかりにくいが「Promise.race は配列の先頭から settled を探して見つけたら終了する」を利用したコード。
;(async () => {
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const p = [
new Promise<string>((resolve) => setTimeout(() => resolve('A'), 4000)),
new Promise<string>((resolve) => setTimeout(() => resolve('B'), 3000)),
new Promise<string>((resolve) => setTimeout(() => resolve('C'), 2000)),
new Promise<string>((resolve) => setTimeout(() => resolve('D'), 1000))
]
console.log(await Promise.race(p))
await wait(1010)
console.log(await Promise.race(p))
await wait(1010)
console.log(await Promise.race(p))
await wait(1010)
console.log(await Promise.race(p))
await wait(1010)
console.log(await Promise.race(p))
})()
$ node --loader ts-node/esm examples/memo/reuse_promise_in_race.ts
D
C
B
A
A
Promise.race は配列の先頭から settled を探して見つけたら終了する
上記の補足。
settled になっている Promise を外し忘れるとおかしなことになりそう。
;(async () => {
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const p = [
new Promise<string>((resolve) =>
setTimeout(() => resolve('rsolved'), 1000)
),
Promise.reject('rejected').catch((r) => r)
]
console.log(await Promise.race(p))
await wait(1010)
console.log(await Promise.race(p))
})()
$ node --loader ts-node/esm examples/memo/promise_race_select_fulfilled.ts
rejected
rsolved
Promise.race で Promise を終了した順番に表示する
Promise の状態(pending かなど)を即座に取得する方法がなさそうだったので少し捻った。
;(async () => {
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const popRace = async (p: Promise<string>[]) => {
let a = [...p]
while (a.length > 0) {
const pa = a.map(
(t, i) =>
new Promise<[string, number]>((resolve) =>
t.then((v) => resolve([v, i]))
)
)
const v = await Promise.race(pa)
console.log(v[0])
a.splice(v[1], 1)
}
}
popRace([
new Promise<string>((resolve) => setTimeout(() => resolve('A'), 300)),
new Promise<string>((resolve) => setTimeout(() => resolve('B'), 200)),
new Promise<string>((resolve) => setTimeout(() => resolve('C'), 400)),
new Promise<string>((resolve) => setTimeout(() => resolve('D'), 100))
])
})()
$ node --loader ts-node/esm examples/memo/reuse_promise_in_race.ts
D
B
A
C
async generator の yield は promise を渡すと await する
TypeScript では Promise<T>
を yield
すると AsyncGenerator<Promise<T>, TReturn, TNext>
か AsyncGenerator<T, TReturn, TNext>
にしないとエラーになるのでそういうものぽい。
いちおう for await...of を使わずに next()
から返ってくる値を使ってみたが、
- Promise が返ってくる
-
await
してみるとvalue
もT
になっている
export {}
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const promiseArray: () => Promise<string>[] = () =>
new Array(5).fill('').map(
(_v, i) =>
new Promise<string>((resolve) => {
setTimeout(() => {
resolve(`done-${i}`)
}, 100 * (i + 1))
})
)
async function* asyncGen(
p: Promise<string>[]
): AsyncGenerator<Awaited<string>, void, void> {
// ): AsyncGenerator<string, void, void> {
// ): AsyncGenerator<Promise<string>, void, void> { これはエラー
await wait(100)
yield p[0]
await wait(100)
yield p[1]
await wait(100)
yield p[2]
await wait(100)
yield p[3]
await wait(100)
yield p[4]
await wait(100)
}
function* syncGen(
p: Promise<string>[]
): Generator<Promise<string>, void, void> {
yield p[0]
yield p[1]
yield p[2]
yield p[3]
yield p[4]
}
;(async () => {
await (async () => {
console.log('async generator with for await...of')
for await (let t of asyncGen(promiseArray())) {
console.log(`${t}`)
console.log(`awaited ${await t}`)
}
})()
await (async () => {
console.log('async generator without for await...of')
const i = asyncGen(promiseArray())
const v = i.next()
console.log(`i.next() = ${v}`)
let t = await v
while (!t.done) {
console.log(`${t.value}`)
t = await i.next()
}
})()
await (async () => {
console.log('sync generator')
for (let t of syncGen(promiseArray())) {
console.log(`${t}`)
console.log(`awaited ${await t}`)
}
})()
await (async () => {
console.log('sync generator with for await...of')
for await (let t of syncGen(promiseArray())) {
console.log(`${t}`)
console.log(`awaited ${await t}`)
}
})()
})()
$ node --loader ts-node/esm examples/memo/async_generator_yeild_await.ts
async generator with for await...of
done-0
awaited don
awaited done-1
done-2
awaited done-2
done-3
awaited done-3
done-4
awaited done-4
async generator without for await...of
i.next() = [object Promise]
done-0
done-1
done-2
done-3
done-4
sync generator
[object Promise]
awaited done-0
[object Promise]
awaited done-1
[object Promise]
awaited done-2
[object Promise]
awaited done-3
[object Promise]
awaited done-4
sync generator with for await...of
done-0
awaited done-0
done-1
awaited done-1
done-2
awaited done-2
done-3
awaited done-3
done-4
awaited done-4
async generator は yeild で awaited になるので generator の外に reject は伝播しない
↑は間違い。
async generator は promise を yeild すると awaited になるので generator の利用側では iterator.next() で reject となる
for await...of を使っても、自分で while などでループさせても同じ。
sync generator では素通りするので引用の通り(「非同期のジェネレーター関数の場合は for await...of を」のところが少し引っかかるのだけど).
同期ジェネレーター関数の finally ブロックが常に呼び出されるようにするには、非同期のジェネレーター関数の場合は for await...of を、同期ジェネレーター関数の場合は for...of を使用し、ループの中で生成されたプロミスを明示的に待つようにしてください
export {}
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const promiseArray: () => Promise<string>[] = () =>
new Array(5).fill('').map(
(_v, i) =>
new Promise<string>((resolve) => {
setTimeout(() => {
resolve(`done-${i}`)
}, 100 * (i + 1))
})
)
async function* asyncGen(
p: Promise<string>[]
): AsyncGenerator<Awaited<string>, void, void> {
try {
await wait(100)
yield p[0]
await wait(100)
yield p[1]
await wait(100)
yield p[2]
await wait(100)
yield p[3]
await wait(100)
yield p[4]
await wait(100)
yield p[5]
await wait(100)
console.log('async generator done')
} finally {
console.log(`async generator: finally`)
}
}
function* syncGen(
p: Promise<string>[]
): Generator<Promise<string>, void, void> {
try {
yield p[0]
yield p[1]
yield p[2]
yield p[3]
yield p[4]
yield p[5]
console.log('sync generator done')
} catch (r) {
console.log(`sync generator: ${r}`)
} finally {
console.log(`sync generator: finally`)
}
}
;(async () => {
await (async () => {
console.log('async generator')
const p = promiseArray()
const r = new Promise<string>((resolve, reject) => {
setTimeout(() => reject('rejected'), 1000)
})
r.catch((r) => console.log(`catch ${r}`))
p.splice(4, 0, r)
try {
for await (let t of asyncGen(p)) {
try {
console.log(`${t}`)
} catch (r) {
console.log(`loop: ${r}`)
throw r
}
}
} catch (r) {
console.log(`for await...of: ${r}`)
}
})()
console.log('---')
await (async () => {
console.log('async generator without for await...of')
const p = promiseArray()
const r = new Promise<string>((resolve, reject) => {
setTimeout(() => reject('rejected'), 1000)
})
r.catch((r) => console.log(`catch ${r}`))
p.splice(4, 0, r)
try {
const i = asyncGen(p)
let t = await i.next()
while (!t.done) {
try {
console.log(`${t.value}`)
t = await i.next()
} catch (r) {
console.log(`loop: ${r}`)
throw r
}
}
} catch (r) {
console.log(`while: ${r}`)
}
})()
console.log('---')
await (async () => {
console.log('sync generator with for await...of')
const p = promiseArray()
const r = new Promise<string>((resolve, reject) => {
setTimeout(() => reject('rejected'), 1000)
})
r.catch((r) => console.log(`catch ${r}`))
p.splice(4, 0, r)
try {
for await (let t of syncGen(p)) {
try {
console.log(`${t}`)
} catch (r) {
console.log(`loop: ${r}`)
throw r
}
}
} catch (r) {
console.log(`for await...of: ${r}`)
}
})()
console.log('---')
await (async () => {
console.log('sync generator with for...of')
const p = promiseArray()
const r = new Promise<string>((resolve, reject) => {
setTimeout(() => reject('rejected'), 1000)
})
r.catch((r) => console.log(`catch ${r}`))
p.splice(4, 0, r)
try {
for (let t of syncGen(p)) {
try {
console.log(`awaited ${await t}`)
} catch (r) {
console.log(`loop: ${r}`)
throw r
}
}
} catch (r) {
console.log(`for...of: ${r}`)
}
})()
})()
$ node --loader ts-node/esm examples/memo/async_generator_reject.ts
async generator
done-0
done-1
done-2
done-3
catch rejected
async generator: finally
for await...of: rejected
---
async generator without for await...of
done-0
done-1
done-2
done-3
catch rejected
async generator: finally
loop: rejected
while: rejected
---
sync generator with for await...of
done-0
done-1
done-2
done-3
catch rejected
for await...of: rejected
---
sync generator with for...of
awaited done-0
awaited done-1
awaited done-2
awaited done-3
catch rejected
loop: rejected
sync generator: finally
for...of: rejected
同期的ジェネレーターと for...of の挙動は Go で Goroutine と Channel を使うパターンからすると「受信側が Channel をクローズしている」ように見えるのでないかと。
できれば generator 側で catch して retrun(generator を終了)させたくなる。
for await...of などでの chatch は reject が発生する前に chain する必要がある
当然と言われればそうなのだがハマりやすいかと。
以下の例では、2 回目の for await...of では順番が回ってくる前の Promise で reject が発生するので
- Unhandled の警告
- それでもループは継続しているので順番がきたら catch される
という結果になっている。
for await...of に限らずに他の方法のループや、単純に await で待っているなどの場合でも同じ。
;(async () => {
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const promiseArray: () => Promise<string>[] = () =>
new Array(5).fill('').map(
(_v, i) =>
new Promise<string>((resolve) => {
setTimeout(() => {
resolve(`done-${i}`)
}, 100 * (i + 1))
})
)
await (async () => {
console.log('timeout = 1000')
const p = promiseArray()
p.splice(
4,
0,
new Promise<string>((resolve, reject) => {
setTimeout(() => reject('rejected'), 1000)
})
)
try {
for await (let t of p) {
console.log(`${t}`)
}
} catch (r) {
console.log(`catch ${r}`)
}
})()
await (async () => {
console.log('timeout = 200')
const p = promiseArray()
p.splice(
4,
0,
new Promise<string>((resolve, reject) => {
setTimeout(() => reject('rejected'), 200)
})
)
try {
for await (let t of p) {
console.log(`${t}`)
}
} catch (r) {
console.log(`catch ${r}`)
}
})()
})()
$ node --loader ts-node/esm examples/memo/need_chain_catch_reject.ts
timeout = 1000
done-0
done-1
done-2
done-3
catch rejected
timeout = 200
done-0
done-1
(node:2165115) UnhandledPromiseRejectionWarning: rejected
(node:2165115) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:2165115) [DEP0018] 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.
done-2
done-3
catch rejected
(node:2165115) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
ループなどで順番待ちになる Promise の catch
上記の対応的なもの。
catch を chain した Promise を渡すと catch からの return が伝播してしまう.
以下の例だと any で無理やりわたしているが、これは undefined が yield される。
(Promise.reject
を return すると Unhandled になる)
別の chain で catch することで for await...of の前で catch できる。
ただし、ループ側で await などが行われると reject 状態になるので(chain が異なるため)、
ループ側でも catch は必要。
;(async () => {
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const promiseArray: () => Promise<string>[] = () =>
new Array(5).fill('').map(
(_v, i) =>
new Promise<string>((resolve) => {
setTimeout(() => {
resolve(`done-${i}`)
}, 100 * (i + 1))
})
)
await (async () => {
console.log('catch in top of chain')
const p = promiseArray()
const r = new Promise<string>((resolve, reject) => {
setTimeout(() => reject('rejected'), 200)
})
const c = r.catch((r) => {
console.log(`catch ${r}`)
})
p.splice(4, 0, c as any)
try {
for await (let t of p) {
console.log(`${t}`)
}
} catch (r) {
console.log(`for await...of ${r}`)
}
})()
console.log('---')
await (async () => {
console.log('catch in another chain')
const p = promiseArray()
const r = new Promise<string>((resolve, reject) => {
setTimeout(() => reject('rejected'), 200)
})
r.catch((r) => {
console.log(`catch ${r}`)
})
p.splice(4, 0, r)
try {
for await (let t of p) {
console.log(`${t}`)
}
} catch (r) {
console.log(`for await...of ${r}`)
}
})()
console.log('---')
})()
$ node --loader ts-node/esm examples/memo/async_generator_catch.ts
catch in top of chain
done-0
done-1
catch rejected
done-2
done-3
undefined
done-4
---
catch in another chain
done-0
done-1
catch rejected
done-2
done-3
for await...of rejected
promise の状態を同期的に取得する方法(API)は無いが、人間が目視で確認はできる
console.log()
で Promise をオブジェクトとして出力すると状態が表示される.
またデバッガーでも表示される.
状態も表示される
ただし toString()
などではできない. これができると古典的な isArray のようなことが出来そうだったのだが.
;(async () => {
let passResolve: (value: string) => void = (v) => {}
const p1 = new Promise<string>((resolve) => {
passResolve = resolve
})
console.log(p1)
console.log(p1.toString())
passResolve('done')
await p1
console.log(p1)
let passReject: (value: string) => void = (v) => {}
const p2 = new Promise<string>((resolve, reject) => {
passReject = reject
}).catch((r) => r)
console.log(p2)
console.log(p2.toString())
passReject('rejected')
await p2
console.log(p2)
})()
$ node --loader ts-node/esm examples/memo/promise_is_pending.ts
Promise { <pending> }
[object Promise]
Promise { 'done' }
Promise { <pending> }
[object Promise]
Promise { 'rejected' }
async generator 経由で Promise (awaitedでない)を渡す
渡すことは他の型か関数でくるむことで対応できる。
(関数を使う方が好みだが、副作用がありそうな気もしないでもない)
ただし、for await...of の await で待たないので reject は sync generator のときと同じような挙動になる。
いわゆる「finally に到達しない問題」はループ内での await で回避できるが、この挙動は「async generator 側で catch できない(generator 側で自身を終了できない)」問題がある。
対応としては async generator 側で catch しておくことだが、これはループ側に reject が伝播してしまう(その方が都合がよさそうだが)。
export {}
const wait = (to: number) =>
new Promise<void>((resolve) => setTimeout(() => resolve(), to))
const promiseArray: () => Promise<string>[] = () =>
new Array(5).fill('').map(
(_v, i) =>
new Promise<string>((resolve) => {
setTimeout(() => {
resolve(`done-${i}`)
}, 100 * (i + 1))
})
)
async function* asyncGenArray(
p: Promise<string>[]
): AsyncGenerator<[Promise<string>], void, void> {
try {
for (let t of p) {
await wait(100)
yield [t]
}
console.log('async generator(array) done')
} catch (r) {
console.log(`async generator(array): ${r}`)
} finally {
console.log(`async generator(array): finally`)
}
}
async function* asyncGenFunc(
p: Promise<string>[]
): AsyncGenerator<() => Promise<string>, void, void> {
try {
for (let t of p) {
await wait(100)
yield () => t
}
console.log('async generator(func) done')
} catch (r) {
console.log(`async generator(func): ${r}`)
} finally {
console.log(`async generator(func): finally`)
}
}
async function* asyncGenCatch(
p: Promise<string>[]
): AsyncGenerator<() => Promise<string>, void, void> {
try {
let err: any
for (let t of p) {
const c = t.catch((r) => {
console.log(`async generatro(catch) loop: ${r}`)
err = r
return Promise.reject(r)
})
if (err) {
break
}
await wait(100)
yield () => c
}
console.log('async generator(catch) done')
} catch (r) {
console.log(`async generator(catch): ${r}`)
} finally {
console.log(`async generator(catch): finally`)
}
}
;(async () => {
await (async () => {
console.log('wrap appray')
try {
for await (let t of asyncGenArray(promiseArray())) {
console.log(`${t[0]}`)
console.log(`awaited ${await t[0]}`)
}
} catch (r) {
console.log(`for await...of: ${r}`)
}
})()
console.log('---')
await (async () => {
console.log('wrap func')
try {
for await (let t of asyncGenFunc(promiseArray())) {
const p = t()
console.log(`${p}`)
console.log(`awaited ${await p}`)
}
} catch (r) {
console.log(`for await...of: ${r}`)
}
})()
console.log('---')
await (async () => {
console.log('reject')
const a = promiseArray()
a.splice(
4,
0,
new Promise<string>((resolve, reject) => {
setTimeout(() => reject('rejected'), 1000)
})
)
try {
for await (let t of asyncGenFunc(a)) {
const p = t()
console.log(`${p}`)
console.log(`awaited ${await p}`)
}
} catch (r) {
console.log(`for await...of: ${r}`)
}
})()
console.log('---')
await (async () => {
console.log('reject catch')
const a = promiseArray()
a.splice(
4,
0,
new Promise<string>((resolve, reject) => {
setTimeout(() => reject('rejected'), 1000)
})
)
try {
for await (let t of asyncGenCatch(a)) {
const p = t()
console.log(`${p}`)
await p
.then((v) => {
console.log(`then ${v}`)
})
.catch((r) => {
console.log(`catch: ${r}`)
})
}
} catch (r) {
console.log(`for await...of: ${r}`)
}
})()
})()
$ node --loader ts-node/esm examples/memo/pass_promise_via_async_generator.ts
wrap appray
[object Promise]
awaited done-0
[object Promise]
awaited done-1
[object Promise]
awaited done-2
[object Promise]
awaited done-3
[object Promise]
awaited done-4
async generator(array) done
async generator(array): finally
---
wrap func
[object Promise]
awaited done-0
[object Promise]
awaited done-1
[object Promise]
awaited done-2
[object Promise]
awaited done-3
[object Promise]
awaited done-4
async generator(func) done
async generator(func): finally
---
reject
[object Promise]
awaited done-0
[object Promise]
awaited done-1
[object Promise]
awaited done-2
[object Promise]
awaited done-3
[object Promise]
async generator(func): finally
for await...of: rejected
---
reject catch
[object Promise]
then done-0
[object Promise]
then done-1
[object Promise]
then done-2
[object Promise]
then done-3
[object Promise]
async generatro(catch) loop: rejected
catch: rejected
async generator(catch) done
async generator(catch): finally
コードブロック的な中で設定した Chain はブロックを抜けても継続して機能する
これも当然と言われればおっしゃる通り。
ループの中でキャンセルのトリガー的に使おうとすると Promise が settled になった時点で
ループ内で設定された Chain がすべて実行される。
- 処理がおわったと思っていたところで不用意にキャンセル処理が実行される
- ループの回数が多ければリソースを圧迫する
ループ内の処理をキャンセルさせる場合は Abort Controller と組み合わせるのが無難か?
- AbortController は明示的にハンドラーを開放できる
- Promise のように
addEventListener
を設定しても既に実行されているabort
には反応しない-
signal.aborted
で判別させる
-
以下のサンプルで最初の setTimeout の値を変更すると挙動の違いがわかる。
const cancel = new Promise<void>((resolve) =>
setTimeout(() => {
console.log('--- first promise is done')
resolve()
}, 3000)
)
console.log('==== start(bare promise)')
for (let idx = 0; idx < 10; idx++) {
const p = ((c: Promise<void>, i: number) =>
new Promise<string>((resolve) => {
let id: any = setTimeout(() => {
id = undefined
resolve(`${i} is done`)
}, 100)
c.then(() => {
console.log(`cancel ${i}`)
if (id) {
id = undefined
clearTimeout(id)
}
resolve(`abort ${i}`)
})
}))(cancel, idx)
await p.then((v) => {
console.log(v)
})
}
console.log('done(bare promise)')
console.log('==== start(AbortController)')
const ac = new AbortController()
cancel.then(() => {
console.log('call ac.abort()')
ac.abort()
})
for (let idx = 0; idx < 10; idx++) {
const p = ((c: Promise<void>, i: number) =>
new Promise<string>((resolve) => {
if (!ac.signal.aborted) {
const handleAbort = () => {
if (id) {
id = undefined
clearTimeout(id)
}
resolve(`cancel ${i}`)
}
let id: any = setTimeout(() => {
id = undefined
ac.signal.removeEventListener('abort', handleAbort)
resolve(`${i} is done`)
}, 100)
ac.signal.addEventListener('abort', handleAbort, { once: true })
} else {
resolve(`aborted ${i}`)
}
}))(cancel, idx)
await p.then((v) => {
console.log(v)
})
}
console.log('done(AbortController)')
export {}
$ node --loader ts-node/esm examples/memo/chain-is-persistent.ts
==== start(bare promise)
0 is done
1 is done
2 is done
3 is done
4 is done
5 is done
6 is done
7 is done
8 is done
9 is done
done(bare promise)
==== start(AbortController)
0 is done
1 is done
2 is done
3 is done
4 is done
5 is done
6 is done
7 is done
8 is done
9 is done
done(AbortController)
--- first promise is done
cancel 0
cancel 1
cancel 2
cancel 3
cancel 4
cancel 5
cancel 6
cancel 7
cancel 8
cancel 9
call ac.abort()
catch とは
ここまであやふあになっていたので。
Unhandled にさせない正常終了せる、という意味では最終的に reject を return しない(または throw させない)。
なお、reject 以外を return すると履行状態で値が設定される。
const p = new Promise((resolve, reject) =>
setTimeout(() => reject('rejected'), 100)
)
console.log('=== return undedined')
try {
const v = p.catch((r) => {
console.log(`catch inner ${r}`)
})
console.log(`resolved ${await v}`)
} catch (r) {
console.log(`catch outer ${r}`)
}
console.log('')
console.log('=== return reject')
try {
const v = p.catch((r) => {
console.log(`catch inner ${r}`)
return Promise.reject(r)
})
console.log(`resolved ${await v}`)
} catch (r) {
console.log(`catch outer ${r}`)
}
console.log('')
console.log('=== return undedined(bare)')
const v1 = p.catch((r) => {
console.log(`catch inner ${r}`)
})
console.log(`resolved ${await v1}`)
console.log('')
console.log('=== return reject(bare)')
const v2 = p.catch((r) => {
console.log(`catch inner ${r}`)
return Promise.reject(r)
})
console.log(`resolved ${await v2}`)
console.log('')
export {}
$ node --loader ts-node/esm examples/memo/behavior-catch.ts
=== return undedined
catch inner rejected
resolved undefined
=== return reject
catch inner rejected
catch outer rejected
=== return undedined(bare)
catch inner rejected
resolved undefined
=== return reject(bare)
catch inner rejected
catch inner rejected
'rejected'
$ echo "${?}"
1