Docs / Language Manual / Async / Await
Edit

Async / Await

Since 10.1

Use the async / await keywords to make asynchronous, Promise based code easier to read and write. If you are already familiar with JS' async / await, you will most likely be able to use the syntax right away as is.

Some basics:

  • You may only use await in async function bodies

  • await may only be called on a promise value

  • await calls are expressions (pattern matching!)

  • 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

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.

Everything we've just saw was essentially what we are used to async / await in JS, but there's 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.

Types and async functions

No promise type in inline return types

When typing the return type of an async function inline, we completely omit the promise<...> type and just state the actual type we want to return. As an example, we would type a logUserDetails function like this:

RES
// Instead of promise<unit> we return `unit` instead. // The boxing into a promise is already done implicitly // by the compiler. let logUserDetails = async (userId: string): unit => { Js.log("...") }

Note: This was a deliberate design decision. More details on the rationale can be found here.

Promises don't auto-collapse in async functions

As a JS developer you'd expect a promise<'a> to collapse into another promise<'a> when returned in an async function. This is not the case in ReScript. Use the await function to unwrap any nested promises 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) }

async function type signatures

Function type signatures (i.e defined in signature files) don't differentiate between async and conventional functions. Every function with a promise return type are async functions; hence we use the 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) }

For completeness reasons, let's also show-case the difference between type definitions and inline type definitions:

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) }

(The last example was only mentioned for education purposes. Don't do that in your actual code.)

Common usage examples

Error handling

As with any synchronous code, you may use try / catch or switch to pattern match on errors.

RES
let logUserDetails = async (userId: string): result<unit, string> => { let email = await fetchUserMail(userId) // await can be used within a `try` body try { Js.log(`Email address for user ${userId}: ${email}`) await sendAnalytics(`User details have been logged for ${userId}`) Ok() } catch { // In case some generic JS exception has been thrown due to unknown interop reasons | JsError(_) => Error("Could not send analytics") } }

Piping await calls

It is possible

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}`)
}

Pattern matching on await calls

Of course we can also go fancy with all kinds of pattern matching combinations.

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)
  }
}