RES@val external fetchUserMail: string => promise<string> = "GlobalAPI.fetchUserMail"
@val external sendAnalytics: string => promise<unit> = "GlobalAPI.sendAnalytics"
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:
// 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
inasync
function bodiesawait
may only be called on apromise
valueawait
calls are expressions, therefore they can be used in pattern matching (switch
)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 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.
RESlet 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.
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)
}
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:
RESlet 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.
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.
@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:
RESlet 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")
}
}