-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Suggestion: supporting asynchronous stack traces for async/await in TypeScript #8392
Comments
It is already possible to replace the implementation of
We do plan to have some broad support for loading helpers from an external helpers library in the future, though nothing quite so granular as you are proposing with |
@rbuckton Do you mean explicitly overwriting async function foo() {
await foo();
return 10;
}
function __awaiter() {
// ...
} |
@hayeah, yes. |
Adding code at the top to replace Also, note you can't just use a function definition like @hayeah did, you need to assign the function with |
Currently you can provide |
Unfortunately I've found replacing The following test case shows it failing:
It produces this stack trace, where line 20 (
On the other hand, creating them in
This way is not only more efficient, but also produces cleaner, complete tracebacks:
The only nuisance with that is that it breaks hoisting, unless it would be handled by TypeScript. |
Promise.coroutine is meant to be called once per function. Calling it per invocation is very slow. |
Background
Currently TypeScript supports the
async
andawait
keywords whenES6
is set as build target, using generators under the hood. This is a promising feature as it allows you to write synchronous looking code that is much easier to write and understand than in a traditional callback based approach. Take for instance this code.Being able to write code in this way is a big step toward making node.js a comfortable and productive platform for writing server applications. Unfortunately, there is a catch, that really is not something new with generators, but anyway that destroys all the productivity you may hoped to get from this feature: Neither node.js or TypeScript implement asynchronous stack traces on exceptions (also known as long stack traces).
As a consequence, if you edit
fun2()
like this, making it fail in an obvious way...You will get this error on runtime:
Not a single trace of
fun1
, which was the actualfun2
caller. It's not hard to guess what this means in codebases larger than this extremely short example: hours ofconsole.log()
calls in every piece of code that may be even remotely related to the error, frustration and finally anger when the source of the error is found, which more often than not is a trivial error on the caller function. Then you wonder how so many people are using node.js for writing web applications and what's wrong with them, suffering in this way.Existing solutions: Bluebird
This issue is not new and fortunately a few people have actually tried to tackle it down. For instance, the promise library bluebird includes support for long stack traces: chained promises get the stack trace of the previous promise, which is added to error traces if a rejection happens.
Here is some code very similar to the TypeScript/ES6 async/await example before, but using
Promise.coroutine()
from Bluebird:It generates this stack trace, which is relatively compact and definitively useful:
Bluebird gotchas
Everything is awesome and beautiful until you mix some old school callback code there...
and suddenly no traceback (well, technically it's still a traceback, but it's almost useless):
... or even promises made with another library, including ES6 native promises.
In this case the trace still preserves the information from previous Bluebird promises but the last one. That one has gone missing since
setTimeout
was called by the nativePromise
constructor.So far Bluebird is a good solution for tracing asynchronous code as long as you always do all asynchronous work inside a promise and don't mix other promise libraries. Fortunately the A+ standard exists to make them replaceable to some extent and many libraries allow changing their promise constructor.
Other solutions
Longjohn
There are other approaches to tackle this problem. For instance, longjohn hooks node.js
EventEmitter
in order to save stack traces the instant asynchronous actions are queued and wraps the callbacks in order to recover the previous traces in case of an error.longjohn is very useful to tackle down the source of errors for dangling callbacks like in the case of
halfBakedPromiser()
before. It's not very adequate to use plain callbacks relying in longjohn as the error catcher though, as it just modifies thestack
property of the error when it escapes the callback and then rethrows it. After that the process dies, killing all active connections in the case of a server (long lived ones e.g. WebSocket included) unless some sort of deprecated hack like node domains is used.longjohn is not able to improve the reporting for
foreignPromise()
and shows an even less useful output if Bluebird long stack traces are disabled.For well behaved promises, when used alone (with
longStackTraces: false
set in Bluebird) longjohn is also able to show a useful traceback. It's different than the one generated by Bluebird in that the same asynchronous function/generator call may appear more than once, once for each time it yielded.When used with
longStackTraces: true
, longjohn conflicts a bit with bluebird, since both add calls to the stack on their own way. longjohn calls appear at the end, afterstartup (node.js)
. In consequence, the generated call list is a bit weird since it actually goes down, up and down again.Asynchronous stack frames (Chrome only)
Chrome debugger has recently implemented support for asynchronous stack traces. This works similarly to longjohn in that it follows code through asynchronous boundaries, but allows full inspecting of the value of the variables in previous frames too. This feature is only enabled when the developer tools are open, so it's not possible to get this enriched stack trace in application code (e.g. to send an automatic bug report).
Proposal
Since there are many ways in which asynchronous code may be structured in an application in order to make error processing and debugging manageable at large scale it may be desirable to be able to adapt TypeScript output code to them when the
async
/await
keywords are used.At the moment TypeScript always emits a polyfill like this in all files using
async
...... with functions like this ...
... being turned into:
A user may want to replace it with a
Promise.coroutine
implementation in order to get sane tracebacks when debugging, like this:Or they may even want to remove the
__awaiter
altogether in order to avoid it popping on the stack trace and avoid creating unecessary closures. In that case this output would be preferred:Replacing the awaiter polyfill could be done quite simply with the
tsconfig.json
. For instance, a new optional propertyawaiterPolyfillDefinition
would store a path relative from the TypeScript project root pointing to a.js
file with the new polyfill.Replacing the how the
async
functions are emitted as JavaScript code in order to remove the__awaiter
call may be harder on the other hand due to hoisting. The.coroutine()
call would have to be hoisted above any usage of the function or otherwise it would be called as a generator instead of as a promise returning function, causing a runtime error.The text was updated successfully, but these errors were encountered: