Docs / Language Manual / Async / Await
Edit

Async / Await

Since 10.1

ReScript comes with async / await support to make asynchronous, Promise based code easier to read and write. This feature is very similar to its JS equivalent, so if you are already familiar with JS' async / await, you will feel right at home.

How it looks

Let's start with a quick example to show-case the syntax:

ReScriptJS Output
// Some fictive functionality that offers asynchronous network actions
@val external fetchUserMail: string => promise<string> = "GlobalAPI.fetchUserMail"
@val external sendAnalytics: string => promise<unit> = "GlobalAPI.sendAnalytics"

// We use the `async` keyword to allow the use of `await` in the function body
let logUserDetails = async (userId: string) => {
  // We use `await` to fetch the user email from our fictive user endpoint
  let email = await fetchUserMail(userId)

  await sendAnalytics(`User details have been logged for ${userId}`)

  Js.log(`Email address for user ${userId}: ${email}`)
}

As we can see above, an async function is defined via the async keyword right before the function's parameter list. In the function body, we are now able to use the await keyword to explicitly wait for a Promise value and assign its content to a let binding email.

You will probably notice that this looks very similar to async / await in JS, but there are still a few details that are specific to ReScript. The next few sections will go through all the details that are specific to the ReScript type system.

Basics

  • You may only use await in async function bodies

  • await may only be called on a promise value

  • await calls are expressions, therefore they can be used in pattern matching (switch)

  • A function returning a promise<'a> is equivalent to an async function returning a value 'a (important for writing signature files and bindings)

  • promise values and types returned from an async function don't auto-collapse into a "flat promise" like in JS (more on this later)

Types and async functions

async function type signatures

Function type signatures (i.e defined in signature files) don't require any special keywords for async usage. Whenever you want to type an async function, use a promise return type.

RESI
// Demo.resi let fetchUserMail: string => promise<string>

The same logic applies to type definitions in .res files:

RES
// function type type someAsyncFn = int => promise<int> // Function type annotation let fetchData: string => promise<string> = async (userId) => { await fetchUserMail(userId) }

BUT: When typing async functions in your implementation files, you need to omit the promise<'a> type:

RES
// This function is compiled into a `string => promise<string>` type. // The promise<...> part is implicitly added by the compiler. let fetchData = async (userId: string): string => { await fetchUserMail("test") }

For completeness reasons, let's expand the full signature and inline type definitions in one code snippet:

RES
// Note how the inline return type uses `string`, while the type definition uses `promise<string>` let fetchData: string => promise<string> = async (userId: string): string { await fetchUserMail(userId) }

Note: In a practical scenario you'd either use a type signature, or inline types, not both at the same time. In case you are interested in the design decisions, check out this discussion.

async uncurried functions

The async keyword does also work for uncurried functions.

RES
let fetchData = async (. userId: string): string { await fetchUserMail(userId) }

Promises don't auto-collapse in async functions

In JS, nested promises (i.e. promise<promise<'a>>) will automatically collapse into a flat promise (promise<'a>). This is not the case in ReScript. Use the await function to manually unwrap any nested promises within an async function instead.

RES
let fetchData = async (userId: string): string => { // We can't just return the result of `fetchUserMail`, otherwise we'd get a // type error due to our function return type of type `string` await fetchUserMail(userId) }

Error handling

You may use try / catch or switch to handle exceptions during async execution.

RES
// For simulation purposes let authenticate = async () => { raise(Js.Exn.raiseRangeError("Authentication failed.")) } let checkAuth = async () => { try { await authenticate() } catch { | Js.Exn.Error(e) => switch Js.Exn.message(e) { | Some(msg) => Js.log("JS error thrown: " ++ msg) | None => Js.log("Some other exception has been thrown") } } }

Note how we are essentially catching JS errors the same way as described in our Exception section.

You may unify error and value handling in a single switch as well:

RES
let authenticate = async () => { raise(Js.Exn.raiseRangeError("Authentication failed.")) } let checkAuth = async () => { switch await authenticate() { | _ => Js.log("ok") | exception Js.Exn.Error(e) => switch Js.Exn.message(e) { | Some(msg) => Js.log("JS error thrown: " ++ msg) | None => Js.log("Some other exception has been thrown") } } }

Important: When using await with a switch, always make sure to put the actual await call in the switch expression, otherwise your await error will not be caught.

Piping await calls

You may want to pipe the result of an await call right into another function. This can be done by wrapping your await calls in a new {} closure.

ReScriptJS Output
@val external fetchUserMail: string => promise<string> = "GlobalAPI.fetchUserMail"

let fetchData = async () => {
  let mail = {await fetchUserMail("1234")}->Js.String2.toUpperCase
  Js.log(`All upper-cased mail: ${mail}`)
}

Note how the original closure was removed in the final JS output. No extra allocations!

Pattern matching on await calls

await calls are just another kind of expression, so you can use switch pattern matching for more complex logic.

ReScriptJS Output
@val external fetchUserMail: string => promise<string> = "GlobalAPI.fetchUserMail"

let fetchData = async () => {
  switch (await fetchUserMail("user1"), await fetchUserMail("user2")) {
  | (user1Mail, user2Mail) => {
      Js.log("user 1 mail: " ++ user1Mail)
      Js.log("user 2 mail: " ++ user2Mail)
    }

  | exception JsError(err) => Js.log2("Some error occurred", err)
  }
}

await multiple promises

We can utilize the Js.Promise2 module to handle multiple promises. E.g. let's use Js.Promise2.all to wait for multiple promises before continuing the program:

RES
let pauseReturn = (value, timeout) => { Js.Promise2.make((~resolve, ~reject) => { Js.Global.setTimeout(() => { resolve(. value) }, timeout)->ignore }) } let logMultipleValues = async () => { let promise1 = pauseReturn("value1", 2000) let promise2 = pauseReturn("value2", 1200) let promise3 = pauseReturn("value3", 500) let all = await Js.Promise2.all([promise1, promise2, promise3]) switch all { | [v1, v2, v3] => Js.log(`All values: ${v1}, ${v2}, ${v3}`) | _ => Js.log("this should never happen") } }

JS Interop with async functions

async / await practically works with any function that returns a promise<'a> value. Map your promise returning function via an external, and use it in an async function as usual.

Here's a full example of using the MDN fetch API, using async / await to simulate a login:

RES
// A generic Response type for typing our fetch requests module Response = { type t<'data> @send external json: t<'data> => promise<'data> = "json" } // A binding to our globally available `fetch` function. `fetch` is a // standardized function to retrieve data from the network that is available in // all modern browsers. @val @scope("globalThis") external fetch: ( string, 'params, ) => promise<Response.t<{"token": Js.Nullable.t<string>, "error": Js.Nullable.t<string>}>> = "fetch" // We now use our asynchronous `fetch` function to simulate a login. // Note how we use `await` with regular functions returning a `promise`. let login = async (email: string, password: string) => { let body = { "email": email, "password": password, } let params = { "method": "POST", "headers": { "Content-Type": "application/json", }, "body": Js.Json.stringifyAny(body), } try { let response = await fetch("https://reqres.in/api/login", params) let data = await response->Response.json switch Js.Nullable.toOption(data["error"]) { | Some(msg) => Error(msg) | None => switch Js.Nullable.toOption(data["token"]) { | Some(token) => Ok(token) | None => Error("Didn't return a token") } } } catch { | _ => Error("Unexpected network error occurred") } }