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
inasync
function bodiesawait
may only be called on apromise
valueawait
calls are expressions (pattern matching!)A function returning a
promise<'a>
is equivalent to anasync
function returning a value'a
(important for writing signature files and bindings)promise
values and types returned from anasync
function don't auto-collapse
How it looks
Let's start with a quick example to show-case the syntax:
// 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.
RESlet 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.
RESlet 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
Pattern matching on await
calls
Of course we can also go fancy with all kinds of pattern matching combinations.
@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)
}
}