Skip to content

Commit 8cd5d98

Browse files
Improve Result class reference and try npm package (#86)
1 parent 1c1f6fa commit 8cd5d98

File tree

4 files changed

+365
-269
lines changed

4 files changed

+365
-269
lines changed

README.md

Lines changed: 90 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
<h1>ECMAScript Try Operator</h1>
44

5-
> [!WARNING]
6-
> After extensive discussion and feedback, the proposal was renamed from `Safe Assignment Operator` to `Try Operator`. _Click here to view the [original proposal](https://github.com/arthurfiorette/proposal-try-operator/tree/old/proposal-safe-assignment-operator)._
5+
> [!TIP]
6+
> You can test the runtime aspect of this proposal and its ergonomics today! Install our reference `Result` class implementation from NPM:
7+
>
8+
> [`npm install try`](https://www.npmjs.com/package/try)
79
810
<br />
911

@@ -17,40 +19,40 @@ This proposal addresses the ergonomic challenges of managing multiple, often nes
1719

1820
Only the `catch (error) {}` block represents actual control flow, while no program state inherently depends on being inside a `try {}` block. Therefore, forcing the successful flow into nested blocks is not ideal.
1921

20-
<details>
21-
22-
<summary>
23-
<h2>Table of Contents</h2>
24-
</summary>
22+
<br />
2523

2624
- [Status](#status)
2725
- [Authors](#authors)
2826
- [Try/Catch Is Not Enough](#trycatch-is-not-enough)
2927
- [Caller's Approach](#callers-approach)
30-
- [What This Proposal Does Not Aim to Solve](#what-this-proposal-does-not-aim-to-solve)
31-
- [Type-Safe Errors](#type-safe-errors)
32-
- [Automatic Error Handling](#automatic-error-handling)
3328
- [Try Operator](#try-operator)
3429
- [Expressions are evaluated in a self-contained `try/catch` block](#expressions-are-evaluated-in-a-self-contained-trycatch-block)
3530
- [Can be inlined.](#can-be-inlined)
3631
- [Any valid expression can be used](#any-valid-expression-can-be-used)
37-
- [`await` is not an exception](#await-is-not-an-exception)
32+
- [`await` is not a special case](#await-is-not-a-special-case)
3833
- [Statements are not expressions](#statements-are-not-expressions)
3934
- [Never throws](#never-throws)
4035
- [Parenthesis Required for Object Literals](#parenthesis-required-for-object-literals)
4136
- [Void Operations](#void-operations)
4237
- [Result Class](#result-class)
38+
- [Reference Implementation](#reference-implementation)
4339
- [Instance Structure](#instance-structure)
44-
- [Iterable](#iterable)
40+
- [Iterable Protocol](#iterable-protocol)
4541
- [Manual Creation](#manual-creation)
42+
- [`try()` static method](#try-static-method)
43+
- [No Result Flattening](#no-result-flattening)
44+
- [What This Proposal Does Not Aim to Solve](#what-this-proposal-does-not-aim-to-solve)
45+
- [Type-Safe Errors](#type-safe-errors)
46+
- [Automatic Error Handling](#automatic-error-handling)
4647
- [Why Not `data` First?](#why-not-data-first)
4748
- [The Need for an `ok` Value](#the-need-for-an-ok-value)
48-
- [Why a Proposal?](#why-a-proposal)
49+
- [A Case for Syntax](#a-case-for-syntax)
50+
- [Why This Belongs in the Language](#why-this-belongs-in-the-language)
4951
- [Help Us Improve This Proposal](#help-us-improve-this-proposal)
5052
- [Inspiration](#inspiration)
5153
- [License](#license)
5254

53-
</details>
55+
<br />
5456

5557
## Status
5658

@@ -70,7 +72,7 @@ _For more information see the [TC39 proposal process](https://tc39.es/process-do
7072

7173
## Try/Catch Is Not Enough
7274

73-
<!-- Credits to https://x.com/LeaVerou/status/1819381809773216099 :) -->
75+
<!-- Credits to https://x.com/LeaVerou/status/1819381809773216099 -->
7476

7577
The `try {}` block often feels redundant because its scoping lacks meaningful conceptual significance. Rather than serving as an essential control flow construct, it mostly acts as a code annotation. Unlike loops or conditionals, a `try {}` block doesn’t encapsulate any distinct program state that requires isolation.
7678

@@ -212,24 +214,6 @@ Ironically, **these are precisely the kinds of functions where improved error ha
212214

213215
<br />
214216

215-
## What This Proposal Does Not Aim to Solve
216-
217-
### Type-Safe Errors
218-
219-
The `throw` statement in JavaScript can throw any type of value. This proposal does not impose nor propose any kind of safety around error handling.
220-
221-
- No generic error type for the proposed [Result](#result-class) class will be added.
222-
- No catch branching based on error type will be added. See [GitHub Issue #43](https://github.com/arthurfiorette/proposal-try-operator/issues/43) for more information.
223-
- No way to annotate a callable to specify the error type it throws will be added.
224-
225-
For more information, also see [microsoft/typescript#13219](https://github.com/Microsoft/TypeScript/issues/13219).
226-
227-
### Automatic Error Handling
228-
229-
While this proposal facilitates error handling, it does not automatically handle errors for you. You will still need to write the necessary code to manage errors the proposal simply aims to make this process easier and more consistent.
230-
231-
<br />
232-
233217
## Try Operator
234218

235219
The `try` operator consists of the `try` keyword followed by an expression. It results in an instance of the [`Result`](#result-class).
@@ -278,7 +262,7 @@ const result = _result
278262
Similar to `void`, `typeof`, `yield`, and `new`:
279263

280264
```js
281-
array.map((fn) => try fn()).filter((result) => result.ok) // works :)
265+
array.map((fn) => try fn()).filter((result) => result.ok)
282266
```
283267

284268
### Any valid expression can be used
@@ -301,18 +285,18 @@ try {
301285
const result = _result
302286
```
303287
304-
#### `await` is not an exception
288+
#### `await` is not a special case
305289
306290
```js
307-
const result = try await fetch("https://api.example.com/data")
291+
const result = try await fetch("https://arthur.place")
308292
```
309293
310-
This is "equivalent" to:
294+
Which is only valid in async contexts and equates to:
311295
312296
```js
313297
let _result
314298
try {
315-
_result = Result.ok(await fetch("https://api.example.com/data"))
299+
_result = Result.ok(await fetch("https://arthur.place"))
316300
} catch (error) {
317301
_result = Result.error(error)
318302
}
@@ -369,7 +353,7 @@ This behavior mirrors how JavaScript differentiates blocks and object literals:
369353
370354
<!-- prettier-ignore -->
371355
```js
372-
{ a: 1 } // empty block with a label
356+
{ a: 1 } // empty block with a label
373357
({ a: 1 }) // object with a key `a` and a number `1`
374358
```
375359
@@ -404,19 +388,28 @@ function work() {
404388
405389
## Result Class
406390
407-
> Please see [`polyfill.d.ts`](./polyfill.d.ts) and [`polyfill.js`](./polyfill.js) for a basic implementation of the `Result` class.
391+
The `try` operator evaluates an expression and returns an instance of the `Result` class, which encapsulates the outcome of the operation.
408392
409-
The `Result` class represents the form of the value returned by the `try` operator.
393+
### Reference Implementation
394+
395+
To validate the ergonomics and utility of this proposal, a spec-compliant, runtime-only implementation of the `Result` class has been published to npm as the [`try`](https://www.npmjs.com/package/try) package. This package provides a `t()` function that serves as a polyfill for the `try` operator's runtime behavior, allowing developers to experiment with the core pattern.
396+
397+
```js
398+
import { t } from "try"
399+
400+
const [ok, err, val] = await t(fetch, "https://arthur.place")
401+
```
402+
403+
You can check the published package at [npmjs.com/package/try](https://www.npmjs.com/package/try) or [github.com/arthurfiorette/try](https://github.com/arthurfiorette/try) and contribute to its development.
410404
411405
### Instance Structure
412406
413-
A `Result` instance contains three properties:
407+
A `Result` instance always contains a boolean `ok` property that indicates the outcome.
414408
415-
- **`ok`**: A boolean indicating whether the expression was executed successfully.
416-
- **`error`**: The error thrown during execution, or `undefined` if no error occurred.
417-
- **`value`**: The data returned from the execution, or `undefined` if an error occurred.
409+
- If `ok` is `true`, the instance also has a `value` property containing the successful result.
410+
- If `ok` is `false`, it has an `error` property containing the thrown exception.
418411
419-
Example usage:
412+
Crucially, a success result does not have an `error` property, and a failure result does not have a `value` property. This allows for reliable checks like `'error' in result`.
420413
421414
```js
422415
const result = try something()
@@ -428,26 +421,54 @@ if (result.ok) {
428421
}
429422
```
430423
431-
### Iterable
424+
### Iterable Protocol
432425
433-
A `Result` instance is iterable, enabling destructuring and different variable names:
426+
To support ergonomic destructuring, `Result` instances are iterable. They yield their state in the order `[ok, error, value]`, allowing for clear, inline handling of both success and failure cases.
434427
435428
```js
436429
const [success, validationError, user] = try User.parse(myJson)
437430
```
438431
439432
### Manual Creation
440433
441-
You can also create a `Result` instance manually using its constructor or static methods:
434+
While the `try` operator is the primary source of `Result` instances, they can also be created manually using static methods. This is useful for testing or for bridging with APIs that do not use exceptions.
442435
443436
```js
444-
// Creating a successful result
445-
const result = Result.ok(value)
437+
// Create a successful result
438+
const success = Result.ok(42)
446439

447-
// Creating an error result
448-
const result = Result.error(error)
440+
// Create a failure result
441+
const failure = Result.error(new Error("Operation failed"))
449442
```
450443
444+
### `try()` static method
445+
446+
It also includes a static `Result.try()` method, which serves as the runtime foundation for the `try` operator. This method wraps a function call, catching any synchronous exceptions or asynchronous rejections and returning a `Result` or `Promise<Result>`, respectively.
447+
448+
The proposed `try expression` syntax is essentially an ergonomic improvement over the more verbose `Result.try(() => expression)`, removing the need for a function wrapper.
449+
450+
### No Result Flattening
451+
452+
The `try` operator and `Result` constructors wrap the value they are given without inspection. If this value is itself a `Result` instance, it will be nested, not flattened. This ensures predictable and consistent behavior.
453+
454+
<br />
455+
456+
## What This Proposal Does Not Aim to Solve
457+
458+
### Type-Safe Errors
459+
460+
The `throw` statement in JavaScript can throw any type of value. This proposal does not impose nor propose any kind of safety around error handling.
461+
462+
- No generic error type for the proposed [Result](#result-class) class will be added.
463+
- No catch branching based on error type will be added. See [GitHub Issue #43](https://github.com/arthurfiorette/proposal-try-operator/issues/43) for more information.
464+
- No way to annotate a callable to specify the error type it throws will be added.
465+
466+
For more information, also see [microsoft/typescript#13219](https://github.com/Microsoft/TypeScript/issues/13219).
467+
468+
### Automatic Error Handling
469+
470+
While this proposal facilitates error handling, it does not automatically handle errors for you. You will still need to write the necessary code to manage errors the proposal simply aims to make this process easier and more consistent.
471+
451472
<br />
452473
453474
## Why Not `data` First?
@@ -522,16 +543,25 @@ For a more in-depth explanation of this decision, refer to [GitHub Issue #30](ht
522543
523544
<br />
524545
525-
## Why a Proposal?
546+
## A Case for Syntax
547+
548+
This proposal intentionally combines the `try` operator with the `Result` class because one is incomplete without the other. The `try` operator standardizes the many attempts at safely catching synchronous function calls (the way we can with Promise `.catch` for async calls). Consistency is key, and the `try` syntax establishes common patterns for all developers.
549+
550+
It has been suggested that a runtime-only proposal for the `Result` class might face less resistance within the TC39 process. While this strategic viewpoint is understood, this proposal deliberately presents a unified feature. Separating the runtime from the syntax severs the solution from its motivating problem. It would ask the committee to standardize a `Result` object whose design is justified by a syntax **that doesn't yet exist**.
551+
552+
Without the `try` operator, the `Result` class is just one of many possible library implementations, not a definitive language feature. We believe the feature must be evaluated on its complete ergonomic and practical merits, which is only possible when the syntax and runtime are presented together.
553+
554+
<br />
555+
556+
## Why This Belongs in the Language
526557
527558
A proposal doesn’t need to introduce a feature that is entirely impossible to achieve otherwise. In fact, most recent proposals primarily reduce the complexity of tasks that are already achievable by providing built-in conveniences.
528559
529-
Optional chaining and nullish coalescing are examples of features that could have remained external libraries (e.g., Lodash's `_.get()` for optional chaining and `_.defaultTo()` for nullish coalescing). However, when implemented natively, their usage scales exponentially and becomes a natural part of developers’ workflows. This arguably improves code quality and productivity.
560+
The absence of a `Result`-like type and a standard pattern for safely wrapping function calls has led to widespread ecosystem fragmentation. The NPM registry contains hundreds of variations attempting to implement safe wrapping of function calls, and countless more exist as private, copy-pasted utilities. This leaves developers with a poor choice: risk adopting a library that may be abandoned, or contribute to the problem by creating yet another bespoke implementation.
530561
531-
By providing such basic conveniences natively, we:
562+
This is the same problem that optional chaining (`?.`) and nullish coalescing (`??`) solved. **They replaced a landscape of competing utilities with a single, trusted language feature**. By standardizing this pattern, we provide a reliable primitive that developers can use with confidence, knowing it is a stable and permanent part of JavaScript.
532563
533-
- Increase consistency across codebases (many NPM packages already implement variations of this proposal, each with its own API and lack of standardization).
534-
- Reduce code complexity, making it more readable and less error-prone.
564+
It also creates a shared foundation between developers and package authors. Everyone can rely on the same Result implementation without compatibility concerns. The goal is to end the fragmentation and establish a foundational tool for robust error handling.
535565
536566
<br />
537567
@@ -548,7 +578,8 @@ This proposal is in its early stages, and we welcome your input to help refine i
548578
- [This tweet from @LeaVerou](https://x.com/LeaVerou/status/1819381809773216099)
549579
- The frequent oversight of error handling in JavaScript code.
550580
- [Effect TS Error Management](https://effect.website/docs/error-management/two-error-types/)
551-
- The [`tuple-it`](https://www.npmjs.com/package/tuple-it) npm package, which introduces a similar concept but modifies the `Promise` and `Function` prototypes—an approach that is less ideal.
581+
- The [`tuple-it`](https://www.npmjs.com/package/tuple-it) npm package, which introduces a similar concept but modifies the `Promise` and `Function` prototypes.
582+
- [Szymon Wygnański](https://finalclass.net) for donating the `try` package name on NPM to host the reference implementation of this proposal.
552583
553584
<br />
554585

polyfill.d.ts

Lines changed: 0 additions & 67 deletions
This file was deleted.

polyfill.js

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)