Library / SDK
arthurfiorette/proposal-try-operator avatar
arthurfiorette/proposal-try-operator

proposal-try-operator: returning errors as values in JavaScript

A proposal to bring ergonomic, inline error handling to JavaScript through a new try operator that always returns a Result.

2,037 stars23 forksUnknownMIT

At a glance

What is it?
proposal-try-operator is a Stage 0 TC39 proposal that adds a try expression operator to JavaScript, converting thrown exceptions into a Result with ok, error and value fields instead of propagating them. A reference implementation is on NPM today, but no engine ships the syntax and the proposal has no champion yet.
Who is it for?
This proposal targets a real pain point in JavaScript: nested try/catch blocks that prevent const assignments and add indentation without adding clarity. Stage 0 means no engine ships the syntax yet, and a TC39 champion is still needed before formal review.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
GitHub does not report a main language for this repository.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Block scoping makes the happy path pay for exception isolation it does not need

JavaScript's try statement has an asymmetry the proposal document describes carefully. A catch block represents genuine alternate control flow, so giving it its own scope makes sense. Try blocks are different: they do not represent a distinct program state the way loops or conditionals do, yet they impose block scoping on the protected code anyway.

The cost shows up in assignments. A const declaration inside a try block cannot be read after the block closes, so callers end up declaring variables with let before the block, then assigning inside it. That defeats the point of const and makes the variable's type uncertain across branches. Static analysis tools and TypeScript narrowing both work better when a binding is fixed after one assignment.

A before-and-after pair in the proposal document makes this concrete. A function calling cache.getUser, db.selectPost and db.selectComments with three nested try/catch blocks forces three let declarations, re-assignments and one additional level of nesting for the post and comments fetch. Rewritten with the proposed operator, all three values are assigned with const, control flow stays linear, and only the exception paths require explicit branching.

The try operator syntax and the Result it always returns

Syntax-wise, the operator mirrors await. Where await expression suspends and resumes an async context, try expression evaluates the expression inside a self-contained try/catch block and always returns a Result object. It never throws, so any function using it can treat error paths as ordinary value checks rather than control-flow interruptions that jump out of the current scope.

A Result carries 3 properties: ok, error and value. When the expression completes without throwing, ok is true, value holds the result and error is undefined. When it throws, ok is false, error holds the caught exception and value is undefined, with the shape fixed regardless of whether the expression is synchronous or asynchronous. That consistency removes one category of difference between sync and async error handling.

Three destructuring forms appear in the proposal. Object destructuring works: const { ok, error, value } = try await fetch("/api/users"). Array destructuring also works: const [ok, fetchErr, res] = try fs.readFileSync("data.txt"). When neither the success check nor the error name is needed immediately, the whole Result can be stored as a single binding instead: const result = try JSON.parse(input). Callers pick whichever form suits local naming conventions.

Testing the proposal today with npm install try

No JavaScript engine ships the operator syntax, so the proposal cannot be used in production today. To experiment with the ergonomics before the syntax exists, the authors publish a reference Result class implementation on NPM.

bash
npm install try

This package runs on Node >= 12 according to the package.json engines field. It lets callers construct Result values manually and wrap operations that might throw, without needing a transpiler or a modified engine. The proposal framing is explicit: "You can test the runtime aspect of this proposal and its ergonomics today." That is a narrower claim than it might appear. What the package exercises is the Result data structure and the destructuring patterns that go with it, not the operator's syntax itself, because try expression is not valid JavaScript in any current runtime.

Code written with the package will use method calls rather than the new operator syntax, so it will not look like the proposal code. Anyone evaluating the proposal for a team decision should test the package first and weigh how much of the ergonomic benefit comes from the operator syntax versus how much comes from the Result data structure alone.

Tuple ordering and what default values do with failed results

A section called "Why Not data First?" in the proposal document explains the ordering. Array form puts ok at index 0, error at index 1, and the value at index 2. Placing the success flag first means a short read for the common check pattern.

Default values work in array destructuring. The proposal's running example includes this line: const [commentsOk, commentsErr, comments = []] = try db.selectComments(post.id, user). If the call fails, comments gets the default value instead of undefined, and the error lands in commentsErr. That lets callers define fallback behavior in the destructuring line itself rather than in a separate conditional assignment.

Manual creation of Result values is also documented. A static try() method on the Result class lets helpers produce Results from non-throwing code paths. Interop is covered too: a dedicated section in the proposal addresses how Results behave when they cross JavaScript context boundaries such as iframes or workers. Full details are at arthur.run/proposal-try-operator. Subsections on the Result instance structure and short forms govern which properties the object carries and how they compose.

await is not a special case, but statements are off limits

A common concern with an operator that looks like await is whether async expressions need special handling. No: await is not a special case in the proposal. Writing try await someAsyncFn() is valid because the entire expression await someAsyncFn() is treated as the input to try, and the operator wraps it. What comes back is still a Result object with the same 3 properties.

Constraints run in the other direction. Statements are not expressions in JavaScript, and the try operator takes an expression, not a statement. Code that uses if, for, while or switch inside the operand position is simply not valid syntax in the JavaScript grammar, because statements introduce their own control flow constructs rather than evaluating to a value. The proposal is explicit: "Statements are not expressions." This is a deliberate boundary aligned with JavaScript's existing grammar, not a gap in the design.

Object literals also require parentheses. An object literal { key: value } is ambiguous at the parser level because the opening brace looks like a block. The proposal requires try ({ key: fn() }) rather than try { key: fn() }. This is the same workaround arrow function returns already use, so it is not a new pattern for JavaScript developers to learn.

Void operations and the sharp edges named in their own subsection

Void operations, those expressions called for their side effects such as writing to a database or dispatching an event, can be wrapped with the try operator. What ends up in the value property is whatever the expression returned, which may be undefined. Including void operations as a valid use case means the proposal does not restrict the operator to expressions that return meaningful data, and any fire-and-forget call that previously swallowed exceptions inside a try block can be rewritten with the same tool.

Sharp edges and hazards have a dedicated subsection in the proposal document. Listing them separately suggests the authors expect some behaviors to surprise callers. A separate section on Precedence and Parsing Notes governs how the operator interacts with adjacent operators in unparenthesized expressions. Those two sections are both at arthur.run/proposal-try-operator and are worth reading before building a Babel plugin or a transpiler for the syntax, since parser edge cases tend to surface in tooling before they appear in production code.

What the proposal does not attempt: typed errors and automatic checking

Two things are explicitly out of scope, and both are listed in a dedicated section of the proposal document. First, type-safe errors. Nothing in the proposal gives the error property a type more specific than the JavaScript base error type, so TypeScript callers who want to know the exact error class before handling it still need to narrow manually with instanceof or a type guard.

Second, automatic error handling. The ok check is a runtime flag, not a compiler requirement. Writing const [, , data] = try fetch(url) and using data without checking ok produces undefined on failure, and nothing in the proposal enforces the check at build time. Languages that make unhandled errors a compile-time type error impose that guarantee through a mechanism the TC39 process cannot add without breaking existing code.

Both limits appear under the heading "What This Proposal Does Not Aim to Solve." They reflect a deliberate choice to stay within what TC39 can add without requiring new toolchain constraints beyond what a transpiler handles today.

Stage 0 with no champion and a last push on 2026-04-02

Currently the proposal sits at Stage 0 in the TC39 process. Its Champion field says: "Actively looking for one." Without a TC39 member champion, the proposal cannot advance to Stage 1, which is where the committee formally agrees to spend resources on the problem and a champion takes responsibility for shepherding it through review meetings.

No GitHub releases exist. The last push was on 2026-04-02, and the repository is not archived. An NPM reference package under the name try is the only form of the proposal that runs in current engines, and it exercises the data structure rather than the operator syntax.

Build tooling in package.json uses ecmarkup at version 23.0.2 and the TC39 ecma262 bibliography at version 2.1.3023, which is the standard toolchain for writing formal TC39 specification text. Running npm run build generates the specification as an HTML file in the build directory. Specification source lives in spec.emu at the repository root. Following the proposal means watching for champion announcements rather than waiting for a release, since there are no releases to watch and Stage 1 entry is the next meaningful milestone.

Editorial conclusion

This proposal targets a real pain point in JavaScript: nested try/catch blocks that prevent const assignments and add indentation without adding clarity. Stage 0 means no engine ships the syntax yet, and a TC39 champion is still needed before formal review. An NPM package under the name try is the only way to evaluate the Result pattern today, using manual construction rather than the operator. Before adopting it, test the package against code you actually write, read the sharp edges section at arthur.run/proposal-try-operator, and check whether a userland library serves your workflow better.

Frequently asked questions

What is the proposal-try-operator?

A Stage 0 TC39 proposal that adds a try expression operator to JavaScript, converting thrown exceptions into a Result object with ok, error and value properties instead of propagating the exception to the calling scope. It is authored by Arthur Fiorette and Arlen Beiler.

Can I use the proposal-try-operator in JavaScript today?

Not as syntax. No engine ships the operator. A reference Result class is published on NPM under the name try, and it runs on Node >= 12, but it uses method calls rather than the proposed syntax and cannot simulate the operator behavior.

How does the proposal-try-operator handle async functions?

The await keyword is not a special case in the proposal. Writing try await someAsyncFn() is valid, with the operator wrapping the awaited expression and returning the same 3-property Result shape as for synchronous expressions.

What does the try operator return?

A Result object with 3 properties: ok (a boolean indicating success), error (the caught error or undefined on success), and value (the expression's return or undefined on failure). It can be destructured as an array in the form [ok, error, value] or as named properties.

Does the proposal-try-operator replace try/catch?

No. The proposal only covers expressions, not statements. The proposal document explicitly lists type-safe errors and automatic error checking as non-goals. Cases requiring blocks, multiple statements, or finally clauses still need the try/catch/finally construct.

Official sources

  1. arthurfiorette/proposal-try-operator on GitHub
  2. Issues
  3. License: MIT
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/arthurfiorette-proposal-try-operator.svg)](https://hysenlabs.com/projects/arthurfiorette-proposal-try-operator)