-
Notifications
You must be signed in to change notification settings - Fork 13.2k
Description
π Search Terms
try operatortry expressionsafe assignment operatorsafe assignment expressiontry keywordwrap an expression that throws
β Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
β 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.