Skip to content

Latest commit

 

History

History
326 lines (248 loc) · 10.3 KB

async-await.mdx

File metadata and controls

326 lines (248 loc) · 10.3 KB
titledescriptioncanonical
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"

Async / Await

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:

<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.

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.

// 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.

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.

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

Error handling

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.

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.

<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!

Pattern matching on await calls

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

await multiple promises

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

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:

// 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") } }
close