Skip to content

Commit 56cccc9

Browse files
committed
update proposal
1 parent 8c822e4 commit 56cccc9

File tree

1 file changed

+137
-4
lines changed

1 file changed

+137
-4
lines changed

text/0004-interoperable-exceptions.md

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,72 @@ Improve exception syntax to ensure exceptions from JavaScript are safely convert
1111

1212
## Motivation
1313

14-
ReScript provides a convenient exception syntax. But it's not really useful.
14+
In the JavaScript/TypeScript world, Exceptions are one of the most akward syntax to use.
1515

16-
However, in the ReScript ecosystem, the `result` type is generally preferred over exceptions, so this syntax is rarely used.
16+
- Exception handling is only available in the try-catch statement, which is not an expression.
17+
- Since JavaScript allows to throw any values, TypeScript only allows `any` or `unknown` as its type.
18+
- Users have to hoist the `let` binding themselves and type inference will not work automatically.
1719

18-
On the other hand, since it's not compatible with JavaScript exceptions, there is no way to handle executions that may throw JavaScript exceptions safely with exception syntax.
20+
### TypeScript example
21+
22+
```ts
23+
function isTypeError(exn: unknown): exn is TypeError {
24+
return exn instanceof TypeError;
25+
}
26+
27+
// Or using third-party validator library for more complex structs.
28+
import * as S from "sury";
29+
30+
// E.g. https://nodejs.org/api/errors.html#nodejs-error-codes
31+
const AccessDeninedErrorSchema = S.schema({
32+
code: S.literal("ERR_ACCESS_DENINED"),
33+
errno: S.number,
34+
message: S.string,
35+
});
36+
37+
let result: ReturnType<typeof maybeThrow>;
38+
try {
39+
result = maybeThrow()
40+
} catch (exn: unknown) {
41+
let result = S.safe(() => S.parseOrThrow(exn, AccessDeninedError));
42+
if (result.sucess) {
43+
let error = exn.value;
44+
// ?^ S.Output<typeof AccessDeninedErrorSchema>
45+
} else if (isTypeError(exn)) {
46+
let error = exn;
47+
// ?^ TypeError
48+
}
49+
throw exn;
50+
}
51+
```
52+
53+
ReScript have way better exception syntax.
54+
55+
```res
56+
exception AccessDeninedError(exn)
57+
exception TypeError(exn)
58+
59+
let result = try {
60+
maybeThrow()
61+
} catch {
62+
| AccessDeninedError(exn) => // ...
63+
| TypeError(exn) => // ...
64+
}
65+
66+
// or in pattern-matching
67+
let result = switch maybeThrow() {
68+
| exception AccessDeninedError(exn) => // ...
69+
| Ok(value) => // ...
70+
| _ => // ...
71+
}
72+
```
73+
74+
But it's not really useful because it's not compatible with JavaScript exceptions, there is no way to handle executions that may throw JavaScript exceptions safely with exception syntax.
1975

2076
## Rationale
2177

78+
Make the error representation checking behavior consistent and customizable with a new attribute syntax.
79+
2280
When the compiler processes a `catch` statement, it performs a runtime type check to ensure that the caught value is compatible with the special runtime representation of the ReScript exception.
2381

2482
Simply by allowing customization of the runtime type checking, we can make ReScript exceptions interoperable with virtually any JavaScript value.
@@ -42,14 +100,25 @@ exception Invalid(t1, t2) when fn
42100

43101
And the identifier in the `when` clause must be a valid binding with type `unknown => bool`.
44102

103+
#### Keyword considerations
104+
105+
Keywords are only used in the context of exceptions, so we are free to choose them.
106+
107+
- `when`: Highlighting works because we've used it in old syntax.
108+
- `if`: It would be suitable for reducing the number of tokens, but it can be confusing because it looks different from an if expression grammar.
109+
- `with`
110+
- `using`
111+
112+
Or use an atribute like `@check(fn)`.
113+
45114
### Semantics
46115

47116
The compiler uses the function in the `when` clause to determine if the caught value can be safely coerced to the expected payload type before entering the specific catch branch.
48117

49118
```res
50119
external isJsError: unknown => bool = "Error.isError"
51120
52-
exception JsError(JsErrot.t) when isJsError
121+
exception JsError(JsError.t) when isJsError
53122
54123
let throwJsError: unit => string = %raw(`() => {
55124
throw new Error();
@@ -83,3 +152,67 @@ try {
83152
}
84153
}
85154
```
155+
156+
## Other considerations
157+
158+
### Canonicalizing exceptions
159+
160+
It is mostly compatible with existing exception representation, except for the payload restrictions.
161+
162+
So this proposal could become the default semantics for exceptions if the existing exception syntax is changed not to allow multiple payloads.
163+
164+
```res
165+
exception ResError // This uses "unit" payload type implicitly
166+
exception ResError(t) // Omit `when` clause to use primitive assertion (e.g. `isReScriptException`)
167+
exception ResError(t, t)
168+
// ^ Gonna be syntax error
169+
170+
exception JsError(JsError.t) when JsError.isJsError
171+
172+
let result = try {
173+
throwError()
174+
} catch {
175+
| ResError(t) => recover(t)
176+
| JsError(error) => recoverFromError(error)
177+
}
178+
```
179+
180+
```js
181+
import * as Primitive_exceptions from "@rescript/runtime/Primitive_exceptions";
182+
import * as JsError from "./JsError.js";
183+
184+
let result;
185+
try {
186+
result = throwError();
187+
} catch (exn) {
188+
if (Primitive_exceptions.isReScriptException(
189+
exn,
190+
// Compiler can pass additional arguments for internal usage.
191+
Symbol.for("Module.ResError"),
192+
)) {
193+
result = recover(exn);
194+
} else if (JsError.isJsError(exn)) {
195+
result = recoverFromError(error)
196+
} else {
197+
throw exn;
198+
}
199+
}
200+
```
201+
202+
203+
## Questions
204+
205+
### Could it use untagged variants?
206+
207+
This is similar to the idea of untagged variants match. If we can make the untagged variants are fully customizable, we could leverage the same mechanism like:
208+
209+
```res
210+
@untagged
211+
type t =
212+
| @check(isResError) ResError(t)
213+
| @check(JsError.isJsError) JsError(JsError.t)
214+
215+
exception ResException(t)
216+
```
217+
218+
However, variant matches must guarantee exhaustiveness; exception matches are not.

0 commit comments

Comments
 (0)