[ANN] Async/await is coming to ReScript!

The time has come to experiment with a much sought after feature for ReScript - dedicated async/await support!

For the past several weeks, @cristiano has been experimenting with what it’d take to implement async/await in ReScript, and it’s a great pleasure to tell you that we have a working prototype.

The goal with this experimentation has been to try and stay as close to the JS equivalent as possible. This effectively means that async/await will, in its final form, look just like the JS equivalent:

let greetUser = async (userId) => {
  let name = await getUserName(. userId)  
  "Hello " ++ name ++ "!"
}

Note: We’re using an uncurried function call here, to show how clean the output can be.

…and the generated JS output for this will emit native async/await syntax:

async function greetUser(userId) {
  var name = await getUserName(userId);
  return "Hello " + name + "!";
}

Release?

We’d like to invite the community to try the protoype and give us feedback. Please join in and tell us your thoughts!
You can test the current prototype by following the instructions found here.

As for a release, provided everything works out, we think async/await could go into the next big release after v10. We will label it an experimental feature in the first release, just to set the expectations appropriately.

Read along for more details on the experiment and everything around it.

The experiment

First, a note: there’s no 1st class syntax support yet in the experiment, but an actual release of async/await will see full 1st class syntax support. It’ll look like the ReScript code snippet at the top of this post. However, in the experiment, it’s implemented as attributes (@async, @await) for convenience of experimenting and testing.

Here’s a commit in the ReScript documentation repository that shows how a function returning a promise is refactored to async/await, including what the JS output ends up being.

Here’s another link to an extensive set of examples that has been tried during the experimentation (and here’s the JS output of that).

More details

Error handling is important, especially in async/await. JS requires wrapping await’s in try/catch to handle errors. ReScript syntax opens up for slightly different error handling than in JS.

Ergonomic syntax for handling errors

Error handling is done by adding a switch case for exception JsError:

let someAsyncFn = async () => {  
  let maybeSomeValue = switch await somethingThatMightThrow() {    
    | data => Some(data)    
    | exception JsError(_) => None
  }
}

This will automatically wrap await somethingThatMightThrow() in a JS try/catch, meaning errors stemming from the await will be caught.

There’s an additional thing we’re exploring that’ll help with error handling too.

Built in static analysis for error handling

We’ve experimented with extending Reanalyze to statically track error handling in async functions. What this means in practice is that Reanalyze can tell you whether you’re handling all potential errors as you’re awaiting values which could throw.

This is another step we’re taking to nudge developers to not forget handling errors in async/await.
It also opens up for adding code actions to our editor tooling that can automatically insert the skeleton for error handling where Reanalyze tells you that you’ve missed handling a potential error.

More details on this will come at a later point.

Compiler warnings for nested promises

This is an improvement in general for promises in the ReScript type system. For those who don’t know, promises in the ReScript type system can be nested (promise-in-promise). However, in runtime the JS engine flattens all nested promises into a single one, meaning any level of promise-in-promise will ultimately produce a single promise resolving to a single value in runtime. This is a problem in ReScript, because the type system can’t flatten promises like that. That leads to potential runtime issues when you have nested promises, unless you add specific runtime code to handle it. Read more on this issue here.

In tandem with the async/await work, @cristiano has built a compiler warning that will catch a lot of these nested promises. This will help us avoid causing runtime issues by warning whenever there’s a nested promise somewhere in your program.

Wrapping up

We’re excited for this, and we hope bringing async/await to ReScript is going to help smoothen the experience for developers adopting ReScript who are used to having async/await in their tool belt from JS/TS.

There’s some work left to finish the v10 release. Still, we look forward to hearing your thoughts, as well as finalizing this feature in time for the next big release after v10.

Looking forward to hearing what you think!

81 Likes

Great works! Looking forward to having this built-in feature soon! Thanks!

7 Likes

Great news, seems like a long-standing feature request is being addressed, and thanks for taking into account the obvious problems with async/promises and solving them. Mostly talking about nested promises and async not returning a Result and wrapping it to expose the JSError. Those seem like a great solution.

Thanks @cristianoc for the implementation but more thanks @zth for the communication, examples and everything in between.

5 Likes

Super excited to see Reanalyze’s exception analysis taking on a bigger role. It’s a very cool piece of tech that I feel is not given enough attention.

11 Likes

WOW! That’s unexpectedly, but great news!

1 Like

What is the return type annotation [for] an async function?

From the examples linked above, it looks like it’s a Js.Promise.t<something>.

1 Like

Ill go back to my purescript tower, but seems unfortunate to do this without atleast the addition/combination with option, Result.t.
async = @do({t=Promise.t, bind=(x) => Js.Promise.then_....})

1 Like

addition/combination with option, Result.t.

You can. There are examples of doing that in the PR.

1 Like

Sorry for missing that! =)
[edit: sorry im not seeing them in there either?]

2 Likes

To get a transparent support of Js.Promise.t<result<...>> and other arbitrary monadic types we would have to loose native async / await output. I think it’s a very good decision to make it work exactly as in JavaScript.

A “do” notation probably can be considered later as a separate feature. To me personally it would be a “fun to have”, while the lack of async / await is a limitation that I feel day to day when working with asynchronous code.

7 Likes

Do you spend a lot of time in your generated javascript?
We’re stuck with it without sourcemaps i guess, but otherwise, I’m particularly not interested in which syntax my generated code uses. // Generated by ReScript, PLEASE EDIT WITH CARE.

1 Like

I tend to be in generated JS often whenever I’ve tried to introduce rescript to any team. But that’s to validate that my bindings are correct, as well as show how easy it is to remove rescript if it doesn’t pan out.

One of the largest impediments has been no async await. So this is a big win!

6 Likes

:star_struck: those kind of feature will boost rescript adoption !

6 Likes

Nice work! Very excited for waiting to use it.

1 Like

Here’s a proof of concept demo of how exception analysis could play with async/await

ezgif.com-gif-maker

20 Likes

Woah that’s sweet. I can’t recall at all if Typescript has an option to enforce exception checks, or if it’s even possible, especially knowing that OCaml 5 hasn’t even implemented it.

TBH the @async and @await decorators are pretty charming. I say release them as is!

5 Likes

Can this behaviour be disabled? For example when I want to panic, I don’t care what type of exception can be thrown. And when it’s an error flow, I think it’s better to use Result.t

You can use Result. There’s nothing to disable.

2 Likes