Skip to content

Commit 57fdf40

Browse files
committed
Update async/await
1 parent b734d6a commit 57fdf40

File tree

1 file changed

+186
-56
lines changed

1 file changed

+186
-56
lines changed

pages/docs/manual/latest/async-await.mdx

+186-56
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,31 @@ description: "Async / await for asynchronous operations"
44
canonical: "/docs/manual/latest/async-await"
55
---
66

7+
import "src/Demo.mjs"
8+
9+
<!-- This prelude is used in many different followup examples, so we use it to shorten the noise of the example code. -->
10+
<div className="hidden">
11+
12+
```res prelude
13+
@val external fetchUserMail: string => promise<string> = "GlobalAPI.fetchUserMail"
14+
@val external sendAnalytics: string => promise<unit> = "GlobalAPI.sendAnalytics"
15+
```
16+
17+
</div>
18+
719
<!-- See https://github.com/cristianoc/rescript-compiler-experiments/pull/1#issuecomment-1131182023 for all async/await use-case examples -->
820

921
# Async / Await
1022

1123
***Since 10.1***
1224

13-
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.
14-
15-
**Some basics:**
16-
- You may only use `await` in `async` function bodies
17-
- `await` may only be called on a `promise` value
18-
- `await` calls are expressions (pattern matching!)
19-
- A function returning a `promise<'a>` is equivalent to an `async` function returning a value `'a` (important for writing signature files and bindings)
20-
- `promise` values and types returned from an `async` function don't auto-collapse
25+
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.
2126

2227
## How it looks
2328

2429
Let's start with a quick example to show-case the syntax:
2530

31+
2632
<CodeTab labels={["ReScript", "JS Output"]}>
2733

2834
```res
@@ -53,41 +59,22 @@ async function logUserDetails(userId) {
5359

5460
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`.
5561

56-
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.
62+
You will probably notice that this looks very similar 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.
5763

58-
## Types and `async` functions
64+
## Basics
5965

60-
### No `promise` type in inline return types
61-
62-
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:
63-
64-
65-
```res
66-
// Instead of promise<unit> we return `unit` instead.
67-
// The boxing into a promise is already done implicitly
68-
// by the compiler.
69-
let logUserDetails = async (userId: string): unit => {
70-
Js.log("...")
71-
}
72-
```
73-
74-
**Note:** This was a deliberate design decision. More details on the rationale can be found [here](https://github.com/rescript-lang/rescript-compiler/pull/5913#issuecomment-1359003870).
75-
76-
### Promises don't auto-collapse in async functions
66+
- You may only use `await` in `async` function bodies
67+
- `await` may only be called on a `promise` value
68+
- `await` calls are expressions, therefore they can be used in pattern matching (`switch`)
69+
- A function returning a `promise<'a>` is equivalent to an `async` function returning a value `'a` (important for writing signature files and bindings)
70+
- `promise` values and types returned from an `async` function don't auto-collapse
7771

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

80-
```res
81-
let fetchData = async (userId: string): string => {
82-
// We can't just return the result of `fetchUserMail`, otherwise we'd get a
83-
// type error due to our function return type of type `string`
84-
await fetchUserMail(userId)
85-
}
86-
```
73+
## Types and `async` functions
8774

8875
### `async` function type signatures
8976

90-
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.
77+
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.
9178

9279
```resi
9380
// Demo.resi
@@ -97,7 +84,7 @@ let fetchUserMail: string => promise<string>
9784

9885
The same logic applies to type definitions in `.res` files:
9986

100-
```res
87+
```res example
10188
// function type
10289
type someAsyncFn = int => promise<int>
10390
@@ -107,42 +94,99 @@ let fetchData: string => promise<string> = async (userId) => {
10794
}
10895
```
10996

110-
For completeness reasons, let's also show-case the difference between type definitions and inline type definitions:
97+
**BUT:** When typing `async` functions in your implementation files, you need to omit the `promise<'a>` type:
98+
99+
```res
100+
// This function is compiled into a `string => promise<string>` type.
101+
// The promise<...> part is implicitly added by the compiler.
102+
let fetchData = async (userId: string): string => {
103+
await fetchUserMail("test")
104+
}
105+
```
106+
107+
For completeness reasons, let's expand the full signature and inline type definitions in one code snippet:
111108

112109
```res
113110
// Note how the inline return type uses `string`, while the type definition uses `promise<string>`
114111
let fetchData: string => promise<string> = async (userId: string): string {
115-
await fetchuserMail(userId)
112+
await fetchUserMail(userId)
116113
}
117114
```
118115

119-
(The last example was only mentioned for education purposes. Don't do that in your actual code.)
116+
**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](https://github.com/rescript-lang/rescript-compiler/pull/5913#issuecomment-1359003870).
120117

121-
## Common usage examples
118+
### `async` uncurried functions
122119

123-
### Error handling
120+
The `async` keyword does also work for uncurried functions.
124121

125-
As with any synchronous code, you may use `try / catch` or `switch` to pattern match on errors.
122+
```res
123+
let fetchData = async (. userId: string): string {
124+
await fetchUserMail(userId)
125+
}
126+
```
126127

127-
```res example
128-
let logUserDetails = async (userId: string): result<unit, string> => {
129-
let email = await fetchUserMail(userId)
128+
### Promises don't auto-collapse in async functions
129+
130+
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.
131+
132+
```res
133+
let fetchData = async (userId: string): string => {
134+
// We can't just return the result of `fetchUserMail`, otherwise we'd get a
135+
// type error due to our function return type of type `string`
136+
await fetchUserMail(userId)
137+
}
138+
```
130139

131-
// await can be used within a `try` body
140+
## Error handling
141+
142+
You may use `try / catch` or `switch` to handle exceptions during async execution.
143+
144+
```res
145+
// For simulation purposes
146+
let authenticate = async () => {
147+
raise(Js.Exn.raiseRangeError("Authentication failed."))
148+
}
149+
150+
let checkAuth = async () => {
132151
try {
133-
Js.log(`Email address for user ${userId}: ${email}`)
134-
await sendAnalytics(`User details have been logged for ${userId}`)
135-
Ok()
152+
await authenticate()
136153
} catch {
137-
// In case some generic JS exception has been thrown due to unknown interop reasons
138-
| JsError(_) => Error("Could not send analytics")
154+
| Js.Exn.Error(e) =>
155+
switch Js.Exn.message(e) {
156+
| Some(msg) => Js.log("JS error thrown: " ++ msg)
157+
| None => Js.log("Some other exception has been thrown")
158+
}
139159
}
140160
}
141161
```
142162

143-
### Piping `await` calls
163+
Note how we are essentially catching JS errors the same way as described in our [Exception](exception#catch-rescript-exceptions-from-js) section.
144164

145-
It is possible
165+
You may unify error and value handling in a single switch as well:
166+
167+
```res
168+
let authenticate = async () => {
169+
raise(Js.Exn.raiseRangeError("Authentication failed."))
170+
}
171+
172+
let checkAuth = async () => {
173+
switch await authenticate() {
174+
| _ => Js.log("ok")
175+
| exception Js.Exn.Error(e) =>
176+
switch Js.Exn.message(e) {
177+
| Some(msg) => Js.log("JS error thrown: " ++ msg)
178+
| None => Js.log("Some other exception has been thrown")
179+
}
180+
}
181+
}
182+
```
183+
184+
**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.
185+
186+
## Piping `await` calls
187+
188+
You may want to pipe the result of an `await` call right into another function.
189+
This can be done by wrapping your `await` calls in a new `{}` closure.
146190

147191
<CodeTab labels={["ReScript", "JS Output"]}>
148192

@@ -164,9 +208,11 @@ async function fetchData(param) {
164208

165209
</CodeTab>
166210

167-
### Pattern matching on `await` calls
211+
Note how the original closure was removed in the final JS output. No extra allocations!
168212

169-
Of course we can also go fancy with all kinds of pattern matching combinations.
213+
## Pattern matching on `await` calls
214+
215+
`await` calls are just another kind of expression, so you can use `switch` pattern matching for more complex logic.
170216

171217
<CodeTab labels={["ReScript", "JS Output"]}>
172218

@@ -208,3 +254,87 @@ async function fetchData(param) {
208254

209255
</CodeTab>
210256

257+
## `await` multiple promises
258+
259+
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:
260+
261+
```res
262+
let pauseReturn = async (value, timeout) => {
263+
Js.Promise2.make((~resolve, ~reject) => {
264+
Js.Global.setTimeout(() => {
265+
resolve(. value)
266+
}, timeout)->ignore
267+
})
268+
}
269+
270+
let logMultipleValues = async () => {
271+
let value1 = await pauseReturn("value1", 2000)
272+
let value2 = await pauseReturn("value2", 1200)
273+
let value3 = await pauseReturn("value3", 500)
274+
275+
let all = await Js.Promise2.all([value1, value2, value3])
276+
277+
switch all {
278+
| [v1, v2, v3] => Js.log(`All values: ${v1}, ${v2}, ${v3}`)
279+
| _ => Js.log("this should never happen")
280+
}
281+
}
282+
```
283+
284+
## JS Interop with `async` functions
285+
286+
`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.
287+
288+
Here's a full example of using the MDN `fetch` API, using `async` / `await` to simulate a login:
289+
290+
```res
291+
// A generic Response type for typing our fetch requests
292+
module Response = {
293+
type t<'data>
294+
@send external json: t<'data> => Promise.t<'data> = "json"
295+
}
296+
297+
// A binding to our globally available `fetch` function. `fetch` is a
298+
// standardized function to retrieve data from the network that is available in
299+
// all modern browsers.
300+
@val @scope("globalThis")
301+
external fetch: (
302+
string,
303+
'params,
304+
) => Promise.t<Response.t<{"token": Js.Nullable.t<string>, "error": Js.Nullable.t<string>}>> =
305+
"fetch"
306+
307+
// We now use our asynchronous `fetch` function to simulate a login.
308+
// Note how we use `await` with regular functions returning a `promise`.
309+
let login = async (email: string, password: string) => {
310+
let body = {
311+
"email": email,
312+
"password": password,
313+
}
314+
315+
let params = {
316+
"method": "POST",
317+
"headers": {
318+
"Content-Type": "application/json",
319+
},
320+
"body": Js.Json.stringifyAny(body),
321+
}
322+
323+
try {
324+
let response = await fetch("https://reqres.in/api/login", params)
325+
let data = await response->Response.json
326+
327+
switch Js.Nullable.toOption(data["error"]) {
328+
| Some(msg) => Error(msg)
329+
| None =>
330+
switch Js.Nullable.toOption(data["token"]) {
331+
| Some(token) => Ok(token)
332+
| None => Error("Didn't return a token")
333+
}
334+
}
335+
} catch {
336+
| _ => Error("Unexpected network error occurred")
337+
}
338+
}
339+
```
340+

0 commit comments

Comments
 (0)