09:46
<Andreu Botella>
A single promise could be branched multiple times, and unhandledrejection events are dispatched for each "unhandled" promise, rather than a single source of rejection
I don't think this is a problem. Context splits are not problematic, only merges are. If you only care about one of the branched promises but not the rest, you could still want to know the source of rejection
09:47
<Andreu Botella>
the way I think of it, if an exception (or promise rejection) is "automatically rethrown" (as might happen conceptually in run), then if you have a way to act on that error, then that context shouldn't be lost without a trace
09:48
<Andreu Botella>
if you choose to catch an exception/rejection and rethrow it, then you might choose to use the original throw context, or switch it depending on your use case
09:48
<Andreu Botella>
the same way you'd wrap with a higher-level exception at API boundaries
09:48
<Andreu Botella>
the inner throw context would be an implementation detail
09:49
<Andreu Botella>
am I making sense?
09:53
<Chengzhong Wu>
the way I think of it, if an exception (or promise rejection) is "automatically rethrown" (as might happen conceptually in run), then if you have a way to act on that error, then that context shouldn't be lost without a trace
What's an "automatical rethrown"?
09:55
<Andreu Botella>
well, I was thinking of how run() essentially rethrows a thrown exception, or how a .then() without a catch handler essentially rethrows a promise rejection, without any user code
10:14
<Chengzhong Wu>

A handled promise could be await-ed or add a new branch with .then, and an unhandledrejection is dispatched for the promise that has no handler instead of the handled rejection source promise.

const p1 = asyncVar.run("p1", () => new Promise((resolve, reject) => {
  reject('rejection')
}))
p1.then(() => {}, () => {}) // handle this promise

const p2 = asyncVar.run("p2", async () => {
  await p1
})

window.addEventListener("unhandledrejection", event => {
  event.promise // => p2
  asyncVar.get() // => ?
})
10:36
<Chengzhong Wu>
I don't think this is a problem. Context splits are not problematic, only merges are. If you only care about one of the branched promises but not the rest, you could still want to know the source of rejection
Like the example above, awaiting a promise of a different context is a merge operation in flow-through
10:37
<Andreu Botella>
I guess both p1 and p2 could be contexts you might want, it's the same pass-around vs pass-through distinction
10:38
<Chengzhong Wu>
yeah, it could be. I'm saying that the flow-around is not something that is not desired.
13:36
<littledan>

I think the flow-through pattern doesn't answer the similar problem of https://github.com/tc39/proposal-async-context/issues/90.

The originating context could be a stack of contexts and use inner-most context would discard all outer contexts. Each await creates resolution handler on a potentially rejected promise, use the inner-most context would lose the context when the rejection was handled multiple times.

let aGlobalPromise = asyncVar.run('global', () => {
  return Promise.reject()
})

async function someAsyncApi() {
  await asyncVar.run('async-inner', async () => {
    try {
      await aGlobalPromise
    } catch (e) {
      throw e
    }
  })
}

asyncVar.run("foo", async () => {
  await someAsyncApi()
});
asyncVar.run("bar", async () => {
  await someAsyncApi()
});

window.addEventListener("unhandledrejection", () => {
  console.log(asyncVar.get());  // 'foo' or 'bar' or 'global' or 'async-inner'?
});
the problem is actually worse than this looks: with any stack of async functions, the unhandled rejection will be credited as throwing from the outermost one, even if there's no catch/finally clause.
13:37
<littledan>
I'm not sure whether the resolve context change would address this; my initial intuition is that it wouldn't
13:38
<littledan>
but maybe the context would be propagated properly through the chain of rejections?
14:19
<Andreu Botella>
but maybe the context would be propagated properly through the chain of rejections?
I think with run it would, because if you're awaiting for a promise that rejects, the rejection can't have the context from before the await, so the only possible context for the current async function's promise to reject with is the reject-time context of the awaited promise
14:19
<Andreu Botella>
with set I'm not sure
14:40
<littledan>
with the current semantics of AsyncContext, the outer promise is the one that's the unhandled rejection, and that's outside of the .run calls which might be deeper in the stack
14:41
<littledan>
an alternative would be to store a snapshot when an Error is constructed or first thrown, and then restore that snapshot later, for example, just like how stack traces work
15:41
<Chengzhong Wu>
20m to the call today, please feel free to add your topics at https://docs.google.com/document/d/1pi-NMbqVhg2UuxQAZ4jOGDeHLlZGD_DJ7fyxHt_C2hs/edit
20:55
<littledan>

Some TODOs I took from the meeting today:

  • Write a spec text on the flow-through pattern to compare on async-sync code differences
  • Compare with other languages like Ruby with open-source APMs that adopt flow-through semantics
  • Give examples of the usefulness/essentialness of enterWith
  • Draft idea for snapshot enterWith and see if it fixes issues
  • Explain in some more detail what the cost of the current problems are (memory to process? Inaccuracy in guessing cause? others?)
22:26
<Justin Ridgewell>
Compare with other languages like Ruby with open-source APMs that adopt flow-through semantics

Collected a few languages: https://github.com/nodejs/node/issues/53037#issuecomment-2136202299
22:41
<Justin Ridgewell>
(Still catching up with everything in this channel over the holiday weekend)
22:48
<Justin Ridgewell>
but it's looking like with the current spec, someAsyncApi().then() would lose track of the rejection context
The current spec should preserve the context at .then() time, no? Are you expecting someAsyncApi’s rejection time?
22:49
<Andreu Botella>
that was indeed my expectation