Skip to content

Proposing a try operator to allow expressions to be protected directly and the result handled normally. #61558

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
6 tasks done
Arlen22 opened this issue Apr 9, 2025 · 5 comments
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript

Comments

@Arlen22
Copy link

Arlen22 commented Apr 9, 2025

πŸ” Search Terms

  • try operator
  • try expression
  • safe assignment operator
  • safe assignment expression
  • try keyword
  • wrap an expression that throws

βœ… Viability Checklist

⭐ Suggestion

Have you ever wanted to catch an error from an expression without interrupting the flow of your code or resorting to cumbersome helper functions and callbacks?

If you're working with sqlite or some other synchronous data situation, you probably want to catch the errors from each specific call and call your error handler with additional information about the request that failed.

Helper functions require callbacks, which require runtime checks to be repeated inside the callback in order for the types to be properly maintained.

Using try/catch requires choosing between scoped const or unscoped var, and it's easy to end up with nested try/catch blocks.

The most common solution is to just put that one line of code in a separate function but if you need to do that for many different calls things start to get tedious, especially if they are all very similar, but not quite the same, with complicated ORM typing that needs to be preserved.

Wouldn't it be easier if we just had a try operator to inline errors and then handle them as part of our normal program?

The try operator would evaluate the expression, and if it throws, it would return the error, clearly marked, rather than throwing.

πŸ“ƒ Motivating Example

Instead of this:

const value = getSomething();
if(!value) return null;
let _result;
try {
  _result = getSomethingElse(value);
} catch(e){
  throw handleError(e);
}
const posts = _result; // assign to const for safety
// ...

or this:

const value = getSomething();
if(!value) return null;
const [ok, err, posts] = try_(() => {
  ok(value) // redundant
  return getSomethingElse(value);
});
if(!ok) throw handleError(err);
// ...

we have this

const value = getSomething();
const [okPosts, errPosts, posts] = try getUsersPosts(value);
if(!okPosts) throw handleError(errPosts);
// ...

or even just this

const value = getSomething();
const posts = handled(try getUserPosts(value));

function handled<T>(a: Result<T>): T { 
  if(!a.ok) throw myErrorHandler(a.error);
  return a.value;
}

And the types are simple.

type ResultInner<Ok, Err, Val> = [Ok, Err, Val] & { ok: Ok; error: Err; value: Val }
type Result<T> = ResultInner<true, undefined, T> | ResultInner<false, any, undefined>

πŸ’» Use Cases

This operator seems unnecessary in the async context, where multiple alternate Promise-based solutions exist or may be easily added, but there is no equivalent in the sync world, and synchronous sqlite and fs access has the most to gain from this feature, as do generators (both sync and async).

It is rare to wrap an entire section of your own code in a try/catch block. But it's common to wrap library calls in try catch blocks to catch the random errors that specific libraries may throw. These errors may be well-defined, but they are not simple to catch and handle inline with the rest of your code.

Currently there are several solutions, but they are cumbersome or require restructuring code flow specifically because the error is possible.

The most obvious alternative would be to use an inline try helper function with an immediate callback which wraps your code in a try/catch. But that breaks runtime type checks on variables in the parent function because Typescript has no way of knowing that the callback is going to be executed immediately.

Obviously a try/catch statment is also possible, but that severely breaks code flow, and most developers are more likely to create a separate function to wrap that one line in a try/catch. This quickly becomes cumbersome if there are a number of similar calls to make.

const test: string;
if(test !== "something") return;
const hello = try_(() => {
  ok(test === "something"); // redundant
  return doSomethingThatCouldThrow(test);
});

In addition, the simplest solution to wrap a yield expression in a generator (in case the consumer calls iter.throw()) is exactly

// const result = try yield something();
const result = yield* try_yield_(function*() { return (
  yield something()
); }, this);

These are the use cases for a dedicated Try Operator that can be used inline. Unlike helper functions, this would wrap an expression in a try at its call site, and return a tuple that preserves type safety while avoiding redundant type checks currently required.

The try operator has the same precedence as the yield operator, which will consume everything it can short of a comma. To prevent confusion with the try block, it may not be immediately followed by { (like the arrow function body expression).

This is already being proposed for Javascript with additional technical details here, of which I am an author.

The strongest objection the authors have gotten is that browser vendors are loath to implement new syntax. Since the most compelling use-case is based on Typescript conventions, I thought perhaps it would be more compatible with the goals of Typescript.

I have already implemented this in Typescript, and it is very simple. The types work well using existing typing, so nothing had to change there. It simply transforms the try keyword into the inline version of one of these four helper functions, as demonstrated in the examples here.

The types are sufficient to allow the tuple or object destructuring usage with the correct type-narrowing (or whatever it's called). Despite the destructuring, the type information is preserved, so if one destructured parameter excludes a type in the union, the other parameters are also narrowed.

While the implementation in Javascript could be more complex, adding this to Typescript already reduces a significant amount of friction and could significantly ease the path to implementation.

This is in no way proposing any changes to how try/catch currently works. The type of the error parameter in the result tuple would be the same as the error parameter in a catch block, whether that's unknown or any.

@RyanCavanaugh
Copy link
Member

This isn't a runtime feature

This is a runtime feature and is out of scope for TypeScript.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Out of Scope This idea sits outside of the TypeScript language design constraints labels Apr 9, 2025
@Arlen22
Copy link
Author

Arlen22 commented Apr 9, 2025

This isn't a runtime feature

This is a runtime feature and is out of scope for TypeScript.

Isn't this the same as things like the compiled generator functions for ES5? Or are those "out of scope" and only introduced if they're being introduced to JavaScript? My example implementation merely replaced the try keyword with an inline function which has the same behavior, which then gets compiled to the target version.

The reason for me bringing it here is that the a big use case for me is the problem with types inside callbacks, since TypeScript can't guarantee that the types still apply. I can't even bring that up in ECMA proposals as a reason to introduce it to JavaScript because according to them it's not a runtime semantic, so I'm sort of stuck.

const test: string;
if(test !== "something") return;
// const hello = try doSomethingThatCouldThrow(test);
const hello = try_(() => {
  ok(test === "something"); // redundant
  return doSomethingThatCouldThrow(test);
});

Replacing that with a try keyword and equivalent runtime JavaScript seemed like the simplest solution.

@RyanCavanaugh
Copy link
Member

Features need to be introduced to JavaScript first, at which point we would (usually) implement downleveling support for them. We don't introduce new syntax that would downlevel to executing JS.

@nmain
Copy link

nmain commented Apr 10, 2025

The reason for me bringing it here is that the a big use case for me is the problem with types inside callbacks, since TypeScript can't guarantee that the types still apply.

You might be interested in #11498

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Out of Scope" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Apr 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants