Skip to content

Commit bd7c3ce

Browse files
committed
update proposal
1 parent 8c822e4 commit bd7c3ce

File tree

1 file changed

+117
-4
lines changed

1 file changed

+117
-4
lines changed

text/0004-interoperable-exceptions.md

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,60 @@ 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+
- Since JavaScript allows to throw any values, TypeScript only allows `any` or `unknown` as its type. Users need to insert their own runtime validation to safely handle exceptions.
17+
- Exception handling is only available in the try-catch statement, which is not an expression.
18+
- Users have to hoist the `let` binding them selves 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 examples
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+
67+
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.
1968

2069
## Rationale
2170

@@ -42,14 +91,23 @@ exception Invalid(t1, t2) when fn
4291

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

94+
#### Keyword considerations
95+
96+
Keywords are only used in the context of exceptions, so we are free to choose them.
97+
98+
- `when`: Highlighting works because we've used it in old syntax.
99+
- `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.
100+
- `with`
101+
- `using`
102+
45103
### Semantics
46104

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

49107
```res
50108
external isJsError: unknown => bool = "Error.isError"
51109
52-
exception JsError(JsErrot.t) when isJsError
110+
exception JsError(JsError.t) when isJsError
53111
54112
let throwJsError: unit => string = %raw(`() => {
55113
throw new Error();
@@ -83,3 +141,58 @@ try {
83141
}
84142
}
85143
```
144+
145+
## Open considerations
146+
147+
### Canonicalizing exceptions
148+
149+
It is mostly compatible with existing exception representation, except for the payload restrictions.
150+
151+
So this proposal could become the default semantics for exceptions if the existing exception syntax is changed not to allow multiple payloads.
152+
153+
```res
154+
exception ResError // This uses "unit" payload type implicitly
155+
exception ResError(t) // Omit `when` clause to use primitive assertion (e.g. `isReScriptException`)
156+
exception ResError(t, t)
157+
// ^ Gonna be syntax error
158+
159+
exception JsError(JsError.t) when JsError.isJsError
160+
161+
let result = try {
162+
throwError()
163+
} catch {
164+
| ResError(t) => recover(t)
165+
| JsError(error) => recoverFromError(error)
166+
}
167+
```
168+
169+
```js
170+
import * as Primitive_exceptions from "@rescript/runtime/Primitive_exceptions";
171+
import * as JsError from "./JsError.js";
172+
173+
let result;
174+
try {
175+
result = throwError();
176+
} catch (exn) {
177+
if (Primitive_exceptions.isReScriptException(
178+
exn,
179+
// Compiler can pass additional arguments for internal usage.
180+
Symbol.for("Module.ResError"),
181+
)) {
182+
result = recover(exn);
183+
} else if (JsError.isJsError(exn)) {
184+
result = recoverFromError(error)
185+
} else {
186+
throw exn;
187+
}
188+
}
189+
```
190+
191+
192+
## Questions
193+
194+
### Is it related to the untagged variants?
195+
196+
This is similar to the idea of making untagged variants customizable using custom assertions.
197+
198+
Variant matches must guarantee exhaustiveness; exception matches are not.

0 commit comments

Comments
 (0)