title | description | canonical |
---|---|---|
Async / Await | Async / await for asynchronous operations | /docs/manual/v11.0.0/async-await |
@valexternalfetchUserMail: string=>promise<string> ="GlobalAPI.fetchUserMail" @valexternalsendAnalytics: string=>promise<unit> ="GlobalAPI.sendAnalytics"
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.
Let's start with a quick example to show-case the syntax:
<CodeTab labels={["ReScript", "JS Output"]}>
// Some fictive functionality that offers asynchronous network actions @valexternalfetchUserMail: string=>promise<string> ="GlobalAPI.fetchUserMail" @valexternalsendAnalytics: string=>promise<unit> ="GlobalAPI.sendAnalytics"// We use the `async` keyword to allow the use of `await` in the function bodyletlogUserDetails=async (userId: string) => { // We use `await` to fetch the user email from our fictive user endpointletemail=awaitfetchUserMail(userId) awaitsendAnalytics(`User details have been logged for ${userId}`) Console.log(`Email address for user ${userId}: ${email}`) }
asyncfunctionlogUserDetails(userId){varemail=awaitGlobalAPI.fetchUserMail(userId);awaitGlobalAPI.sendAnalytics("User details have been logged for "+userId+"");console.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.
- You may only use
await
inasync
function bodies await
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)
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.
// Demo.resi let fetchUserMail: string => promise<string>
The same logic applies to type definitions in .res
files:
// function typetypesomeAsyncFn=int=>promise<int> // Function type annotationletfetchData: string=>promise<string> =async (userId) => { awaitfetchUserMail(userId) }
BUT: When typing async
functions in your implementation files, you need to omit the promise<'a>
type:
// This function is compiled into a `string => promise<string>` type.// The promise<...> part is implicitly added by the compiler.letfetchData=async (userId: string): string=> { awaitfetchUserMail("test") }
For completeness reasons, let's expand the full signature and inline type definitions in one code snippet:
// Note how the inline return type uses `string`, while the type definition uses `promise<string>`letfetchData: string=>promise<string> =async (userId: string): string { awaitfetchUserMail(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.
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.
letfetchData=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`awaitfetchUserMail(userId) }
You may use try / catch
or switch
to handle exceptions during async execution.
// For simulation purposesletauthenticate=async () => { raise(Exn.raiseRangeError("Authentication failed.")) } letcheckAuth=async () => { try { awaitauthenticate() } catch { | Exn.Error(e) =>switchExn.message(e) { | Some(msg) =>Console.log("JS error thrown: "++msg) | None=>Console.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:
letauthenticate=async () => { raise(Exn.raiseRangeError("Authentication failed.")) } letcheckAuth=async () => { switchawaitauthenticate() { | _=>Console.log("ok") | exceptionExn.Error(e) =>switchExn.message(e) { | Some(msg) =>Console.log("JS error thrown: "++msg) | None=>Console.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.
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.
<CodeTab labels={["ReScript", "JS Output"]}>
@valexternalfetchUserMail: string=>promise<string> ="GlobalAPI.fetchUserMail"letfetchData=async () => { letmail= {awaitfetchUserMail("1234")}->String.toUpperCaseConsole.log(`All upper-cased mail: ${mail}`) }
asyncfunctionfetchData(param){varmail=(awaitGlobalAPI.fetchUserMail("1234")).toUpperCase();console.log("All upper-cased mail: "+mail+"");}
Note how the original closure was removed in the final JS output. No extra allocations!
await
calls are just another kind of expression, so you can use switch
pattern matching for more complex logic.
<CodeTab labels={["ReScript", "JS Output"]}>
@valexternalfetchUserMail: string=>promise<string> ="GlobalAPI.fetchUserMail"letfetchData=async () => { switch (awaitfetchUserMail("user1"), awaitfetchUserMail("user2")) { | (user1Mail, user2Mail) => { Console.log("user 1 mail: "++user1Mail) Console.log("user 2 mail: "++user2Mail) } | exceptionJsError(err) =>Console.log2("Some error occurred", err) } }
asyncfunctionfetchData(param){varval;varval$1;try{val=awaitGlobalAPI.fetchUserMail("user1");val$1=awaitGlobalAPI.fetchUserMail("user2");}catch(raw_err){varerr=Caml_js_exceptions.internalToOCamlException(raw_err);if(err.RE_EXN_ID==="JsError"){console.log("Some error occurred",err._1);return;}throwerr;}console.log("user 1 mail: "+val);console.log("user 2 mail: "+val$1);}
We can utilize the Promise
module to handle multiple promises. E.g. let's use Promise.all
to wait for multiple promises before continuing the program:
letpauseReturn= (value, timeout) => { Promise.make((resolve, _reject) => { setTimeout(() => { resolve(value) }, timeout)->ignore }) } letlogMultipleValues=async () => { letpromise1=pauseReturn("value1", 2000) letpromise2=pauseReturn("value2", 1200) letpromise3=pauseReturn("value3", 500) letall=awaitPromise.all([promise1, promise2, promise3]) switchall { | [v1, v2, v3] =>Console.log(`All values: ${v1}, ${v2}, ${v3}`) | _=>Console.log("this should never happen") } }
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:
// A generic Response type for typing our fetch requestsmoduleResponse= { typet<'data> @sendexternaljson: 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") externalfetch: ( string, 'params, ) =>promise<Response.t<{"token": Nullable.t<string>, "error": 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`.letlogin=async (email: string, password: string) => { letbody= { "email": email, "password": password, } letparams= { "method": "POST", "headers": { "Content-Type": "application/json", }, "body": Json.stringifyAny(body), } try { letresponse=awaitfetch("https://reqres.in/api/login", params) letdata=awaitresponse->Response.jsonswitchNullable.toOption(data["error"]) { | Some(msg) =>Error(msg) | None=>switchNullable.toOption(data["token"]) { | Some(token) =>Ok(token) | None=>Error("Didn't return a token") } } } catch { | _=>Error("Unexpected network error occurred") } }