You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: _blogposts/2025-09-01-let-unwrap.mdx
+68-26Lines changed: 68 additions & 26 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,16 +4,12 @@ date: "2025-09-01"
4
4
badge: roadmap
5
5
title: let?
6
6
description: |
7
-
A new let-unwrap syntax just landed in ReScript.
7
+
A new let-unwrap syntax just landed in ReScript. Experimental!
8
8
---
9
9
10
10
After long discussions we finally decided on an unwrap syntax for both the `option` and `result` types that we are happy with and that still matches the explicitness of ReScript we all like.
11
11
12
-
# What is it exactly?
13
-
14
-
`let?` or `let-unwrap` is a tiny syntax that unwraps `result`/`option` values and *early-returns* on `Error`/`None`. It’s explicitly **experimental** and **disabled by default** behind a new “experimental features” gate.
15
-
16
-
### Example
12
+
`let?` or `let-unwrap` is a tiny syntax that unwraps `result`/`option` values and *early-returns* on `Error`/`None`. It’s explicitly **experimental** and **disabled by default** behind a new "experimental features" gate. See below how to enable it.
17
13
18
14
Before showing off this new feauture, let's explore why it is useful. Consider a chain of `async` functions that are dependent on the result of the previous one. The naive way to write this in modern ReScript with `async`/`await` is to just `switch` on the results.
19
15
@@ -35,26 +31,24 @@ let getUser = async id =>
35
31
36
32
Two observations:
37
33
1. with every `switch` expression, this function gets nested deeper.
38
-
2. The `Error` branch of every `switch` is just an identity mapper (neither wrapper nor contents change)
34
+
2. The `Error` branch of every `switch` is just an identity mapper (neither wrapper nor contents change).
39
35
40
-
This means even though `async`/`await` syntax is available in ReScript for some time now, it is also understandable that people created their own `ResultPromise` libraries to handle such things with less lines of code, e.g.:
36
+
The only alternative in ReScript was always to use some specialized methods:
While this is much shorter, it is also harder to understand because we have two wrapper types here, `promise` and `result`. And we have to wrap the non-async type in a `Promise.resolve` in order to stay on the same type level.
45
+
**Note**: `Result.flatMapOkAsync` among some other async result helper functions are brand new in ReScript 12 as well!
46
+
47
+
This is arguably better, more concise, but also harder to understand because we have two wrapper types here, `promise` and `result`. And we have to wrap the non-async type in a `Promise.resolve` in order to stay on the same type level. Also we are giving up on our precious `async`/`await` syntax here.
48
+
49
+
## Introducing `let?`
50
+
51
+
Let's rewrite the above example again with our new syntax:
58
52
59
53
```rescript
60
54
let getUser = async (id) => {
@@ -64,19 +58,67 @@ let getUser = async (id) => {
64
58
Ok(decodedUser)
65
59
}
66
60
```
67
-
With the new `let-unwrap` syntax, `let?` in short, we now have to follow the happy-path (in the scope of the function). And it's immediately clear that `fetchUser` is an `async` function while `decodeUser` is not. There is no nesting as the `Error` is automatically mapped. But be assured the error case is also handled as the type checker will complain when you don't handle the `Error` returned by the `getUser` function.
61
+
62
+
This strikes a balance between conciseness and simplicity that we really think fits ReScript well.
63
+
64
+
With `let?`, we can now safely focus on the the happy-path in the scope of the function. There is no nesting as the `Error` is automatically mapped. But be assured the error case is also handled as the type checker will complain when you don't handle the `Error` returned by the `getUser` function.
65
+
66
+
This desugars to a **sequence** of early-returns that you’d otherwise write by hand, so there’s **no extra runtime cost** and it plays nicely with `async/await` as the example above suggests.
67
+
68
+
Of course, it also works for `option` with `Some(...)`.
69
+
70
+
```rescript
71
+
let getActiveUser = user => {
72
+
let? Some(name) = activeUsers->Array.get(user)
73
+
Some({name, active: true})
74
+
}
75
+
```
76
+
77
+
It also works with the unhappy path, with `Error(...)` or `None` as the main type and `Ok(...)` or `Some(...)` as the implicitly mapped types.
78
+
79
+
```rescript
80
+
let getNoUser = user => {
81
+
let? None = activeUsers->Array.get(user)
82
+
Some("No user for you!")
83
+
}
84
+
85
+
let decodeUserWithHumanReadableError = user => {
86
+
let? Error(_e) = decodeUser(user)
87
+
Error("This could not be decoded!")
88
+
}
89
+
```
90
+
91
+
Beware it targets built-ins only, namely `result` and `option`. Custom variants still need `switch`. And it is for block or local bindings only; top-level usage is rejected.
92
+
93
+
```rescript
94
+
let? Ok(user) = await fetchUser("1")
95
+
// ^^^^^^^ ERROR: `let?` is not allowed for top-level bindings.
0 commit comments