Skip to content
This repository has been archived by the owner on Mar 9, 2021. It is now read-only.

[async/await] allow state machine to re-throw errors #423

Open
nippur72 opened this issue May 30, 2015 · 7 comments
Open

[async/await] allow state machine to re-throw errors #423

nippur72 opened this issue May 30, 2015 · 7 comments

Comments

@nippur72
Copy link
Contributor

I'm stuck on this problem when trying to make async/await work seamlessly with JavaScript promises (kriskowal/Q library, but I guess it's the same with other promise libraries as well).

The problem is that exceptions inside a promise are captured and eventually rethrown on the "next tick" of the event loop (with a setTimeout() call). Unfortunately, that causes that they are not caught by the state machine try...catch block, making them impossible to handle.

I tried all possible tweaks, but despite all my efforts I was unable to make it work.

The only possible solution I've found requires a very small modification to the current state machine:

try {
   if (ex != undefined) throw ex;   // <- this line added
   for (;;) {

This would allow to re-enter the state machine and rethrow from local site any caught error.

And to take advantage of this modification, one would have to:

  • set ex variable to exception that needs to be rethrown
  • reenter the state machine with a $sm() call

For example with this custom GetAwaiter() from within a Future object:

[InlineCode("({this}.catch(function(err){{ ex=err; $sm(); }})")] 
public extern Future<T> GetAwaiter();

that transpiles into:

$t1 = $myPromise.catch(function (error) {                                                   
   ex = error;  // sets error in state machine "ex" variable
   $sm();  // re-enter the state machine and throw
});
//...
$t1.done($sm);  // <- OnCompleted()/continueWith() rewritten

So my question is, does the above modification make any sense? Is it acceptable/doable? Is there a better way to handle all this?

Any suggestion is welcome.

The final goal of this effort is to use JavaScript promises with await without breaking the current semantic.

@erik-kallen
Copy link
Contributor

I guess this would be possible to do by creating a custom implementation of IRuntimeLibrary.

@nippur72
Copy link
Contributor Author

nippur72 commented Jun 1, 2015

Uhm, sounds rather complex.

Actually I've fixed it by forking the repo and having the following added in StateMachineRewriter.cs

IEnumerable<JsVariableDeclaration> declarations = new[] 
{
   JsStatement.Declaration(_stateVariableName, JsExpression.Number(0)),
   JsStatement.Declaration("$$ex", null) // <- adds: "var $$ex;" 
};  

...

// adds: if ($$ex !== undefined) throw $$ex;   
var ifs = JsStatement.If(
   JsExpression.NotSame(JsExpression.Identifier("$$ex"),JsExpression.Identifier("undefined")),
      JsStatement.Throw(JsExpression.Identifier("$$ex")),
      null);
guarded = JsStatement.Block(ifs, guarded);
currentBlock.Add(JsStatement.Try(guarded, @catch, @finally));

So any GetAwaiter() interested in re-throwing captured exceptions, can set the "$$ex" variable and re-enter the state machine function (via inline code).

Has this workaround any chance of being supported officially?

@erik-kallen
Copy link
Contributor

Maybe. How generic is the use case? Is it useful for other use cases as well or is it very tailored to Q? If it has any kind of generic value, I guess we could do this, but only if the $$ex variable is used in the body.

@nippur72
Copy link
Contributor Author

nippur72 commented Jun 3, 2015

I did a quick implementation of another promise library (lahmatiy/es6-promise-polyfill) just to see if it's a generic case.

Well, the workaround was needed with this library too (and generally speaking, it's needed to all libraries that follow the ES6 promise specs).

To sum up the problem:

  • to convert a JS promise into a C# awaitable, you need to add a .catch() in the promise chain that throws an error that can be later catched by the state machine. For example: .catch(function(err){throw err;})
  • this is not possible because of:
    • according to es6 specs, .catch() does capture all exceptions
    • even if you are able to raise exceptions (with .done of Q library) they are thrown asynchronously at the next tick of the event loop, and thus outside of the state machine

The "workaround":

  • allows a custom GetAwaiter() to add a special .catch() handler in the promise chain that:
    • sets the captured exception in the $$ex variable of the state machine
    • reenters the state machine, allowing it to re-throw

example

    [InlineCode("{this}.catch(function(err){{$$ex=err; $sm();}})")]     
    public extern Promise GetAwaiter();

Of course to make it work, any promise implementer has to adhere strictly to this behaviour.

I'll prepare a PR so we can discuss it more in detail (btw, I haven't found a JsConstant.Undefined, is there one?).

nippur72 added a commit to nippur72/SaltarelleCompiler that referenced this issue Jun 3, 2015
@erik-kallen
Copy link
Contributor

OK, so then it is probably a good idea to do this.

I do, however, think that we should only declare the $$ex variable if it is actually used.

@nippur72
Copy link
Contributor Author

Unfortunately I have no idea how to detect when $$ex is used... should we scan the JsStatements?

@erik-kallen
Copy link
Contributor

The idea to do this is to derive from RewriterVisitorBase and override the VisitIdentifier method to set a flag when locating a node of interest (an identifier whose name is $$ex). See for example Saltarelle.Compiler.JSModel.StateMachineRewrite.UsesThisVisitor.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants