Open Not-Jayden opened 2 months ago
@alexanderhorner then how is this different from just returning the error?
This is about handling errors. For example
fetch
throws errors in some scenarios. That's howfetch
was designed. You can't change it to return errors instead of throwing them. You can however make alternative ways totry ... catch
blocks to better handle them. This is what this proposal is about.Then, it seems like a very weak case for adding new syntax compared to #9 or #15. If this is really that important, I am closer to @nektroās original proposal above. It is just a variation of Promise
.catch
which also extends to non-Promise values. It is unopinionated how the data is actually handled and is very similar to the existing usages.
This completely ignores the annoyance with return values, closures etc.
If every proposal would be shot down by the "it's just a small improvement, not worth it"-crowd, we would still be stuck with jquery and in callback hell...
This completely ignores the annoyance with return values, closures etc.
Maybe I missed those concerns? It would help if you can describe the problems with it.
If this is a syntax change, why even relying on tuple or objects?
const error or data = try foo()
I mean, I'm not at all bound to the "or" syntax above, just I feel we could bring clarity and constraint if we be a bit less bound to known structures...
Also, as for typing, that's TypeScript's problem then, ain't it?
function foo(): ResultType or throws ErrorType {}
Not something we should care in here I suppose?
fetch example from above with my version (the use of the
catch
keyword is illustrative to avoid bikeshed)const fetchAlternative = async () => { const response = await fetch('https://api.example.com/data') catch (fetchError) { console.error('Failed to fetch:', fetchError); return []; }; const json = await response.json() catch (parseError) { console.error('Failed to parse JSON:', parseError); return []; }; return json.items || []; }; // Usage fetchAlternative().then(items => console.log('Fetched items:', items));
I really like this approach, it reminds me to Zig or Rust's Result.unwrap_or_else method. I think would add a way to set a default value for the variable when there's is an error.
Something like:
const result = divide(10, divisor) catch (error) {
console.error("failed to divide:", error);
default 0; // assign 0 to result
}
console.log(result); // output should be the result of the math if successful or 0 if error
why
[error, data]
, not[data, error
]?
Placing the error before the data, I guess, is to emphasize the importance of the error.
I wonder whether { error, data }
could be a viable option instead of a tuple where order matters š¤
@manniL worth checking out the settled result from Promise.allSettled, matches but uses the word reason instead of error.
A rejection or caught value may not be an error
Have detailed a Promise.settled alternative above for promise values.
I also dislike using
try
andcatch
for the reasons previously mentioned.
I think it's funny that people are pushing back on using existing keywords (a fair concern) but we also resist inventing an entirely new keyword.
To me, this illuminates how spiritually similar this new idea is to the thing it is replacing. try
is what we want to do (give it a try).
Someone suggested trycatch
, which at that point, why not a totally new keyword, like attempt
or safely
?
I think the ?
also must feel exhausted at this point as well.
I wonder whether
{ error, data }
could be a viable option instead of a tuple where order matters š¤
Is there a scenario where it would return both data and errors? If not, then why not have it just store the error as an object of the response?
I wonder whether
{ error, data }
could be a viable option instead of a tuple where order matters š¤
Yeah like an npm library called await to
that I can't find now š¤:
async function to(promise) {
const response = { data: null, error: null }
try {
response.data = await Promise.resolve(promise)
} catch (error) {
response.error = error
}
return response
}
const { error, data } = await to(somePromise())
Promise.withResolvers
does this:
const {promise, resolve, reject} = Promise.withResolvers()
@angelhdzmultimedia, https://github.com/arthurfiorette/tuple-it
@angelhdzmultimedia, https://github.com/arthurfiorette/tuple-it
But that returns array/tuple. We talking about an object with props. š¤·āāļø
it doesnt return an object for the same reason result
doesnt go first. would make it to easy to implicitly ignore the error and would make reading the code harder
it doesnt return an object for the same reason
result
doesnt go first. would make it to easy to implicitly ignore the error and would make reading the code harder
I agree with you in spirit, but doesn't this whole thing risk implicitly ignoring the error? Unless I'm missing something, there's nothing catching the error and forcing a reaction to it. This feels more like optional chaining where part of the likely value is avoiding error handling by saying "do this and just assume I don't want it to break if there's a problem". (Legit not clear on how this forces better error handling)
Why a new word in the middle? I think that the proposal for both the operator and this keyword are unnecessary. Right now I don't need the approval of the proposal to achieve the same thing. Even this can be done in many ways, this is just an example:
const attempt = (operation) => {
let result = null;
let error = null;
try {
result = operation();
} catch(err) {
error = err;
}
return [error, result];
};
const attemptAsync = async (operation) => {
let result = null;
let error = null;
try {
result = await operation();
} catch(err) {
error = err;
}
return [error, result];
};
const [error, result] = attempt(() => {
// ...
});
// or
const [error, result] = attemptAsync(async () => {
// ... await something
});
See #24
What makes you think proposals are only for currently unachievable things?
I rather like the idea of leaning more towards expression rather than assignment position so that you could do things like:
function cb(result: SafeResult);
cb(try ! JSON.parse("NOT JSON"))
not really commenting on the actual syntax but any syntax that allows that position would be nice. Also being in an expression position it would allow things like utilities ala ().unwrap_or_else
one problem with prefix based operation is chaining will require ()
to disambiguate ordering for things if helpers are provided.
try
won.@arthurfiorette
What makes you think proposals are only for currently unachievable things?
The purpose of proposals is not limited to addressing things that are currently unachievable. Proposals in programming languages are valuable for exploring enhancements, but they should add significant benefits without increasing complexity or redundancy. While the ?=
operator and the try
keyword aim to streamline error handling, it's important to assess whether they truly improve JavaScript, which already has robust mechanisms for managing errors explicitly.
Other languages, like Go and Rust, handle errors with specific patterns deeply integrated into their design. For example, Go returns a tuple with a result and an error that must be explicitly managed. But this only means that a function can return a tuple:
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
Rust uses the Result type and ? operator, leveraging its strong type system. But in the context of Rust, the ? operator does not handle errors in the traditional sense (as a try-catch
block would in other languages), but is instead a concise way to check the value of an expression and act accordingly.
let result = divide(10.0, 0.0)?;
These languages are compiled and have different execution models from JavaScript, which is interpreted and dynamic. Introducing similar features in JavaScript could lead to a mismatch with the language's design and add unnecessary complexity. It's essential to evaluate if these changes align with JavaScript's core principles, rather than adopting features simply because they work well in other contexts.
What in "transforming the error as value" says it can only be achieved with strong typed languages? I'm not trying to port Rust's Result into a JS class...
That's just a better way to the callee of a function be explict about its possibility of an error being thrown as well as a easier way to handle them without the try {} block scoping which has no actual use besides forcing us to either indent or declare reassignable variables.
@arthurfiorette Are you saying this proposal's motivation is to avoid indenting code or using re-assignable variables?
If that's the case than the Write-Once const Declarations proposal would solve solve that problem, as I mentioned in #31.
Otherwise can you please clarify the problem this proposal is trying to solve (with examples)?
Is it just syntactic sugar to avoid writing try-catch
blocks?
@arthurfiorette
What in "transforming the error as value" says it can only be achieved with strong typed languages? I'm not trying to port Rust's Result into a JS class...
That's just a better way to the callee of a function be explict about its possibility of an error being thrown as well as a easier way to handle them without the try {} block scoping which has no actual use besides forcing us to either indent or declare reassignable variables.
Thank you for explaining your perspective. I understand that your proposal isn't about porting features from strongly-typed languages, but rather simplifying error handling in JavaScript.
However, while transforming errors into values can work in dynamic languages like JavaScript, it risks losing the clarity that try-catch
blocks provide. These blocks, though sometimes verbose, make error handling explicit and easy to follow, which is crucial in a language like JavaScript.
It's also important to remember that functions are generally expected to return specific types of data as their normal operation. Exceptions, on the other hand, are meant for exceptional cases and should not be part of the standard return value of a function. They serve to interrupt the normal flow when something goes wrong, rather than being integrated into the functionās output.
Your idea to reduce indentation and the need for re-assignable variables is interesting, but we should consider whether this trade-off might complicate the code in other ways. The goal should be to enhance the language while maintaining clear and robust error handling practices.
I appreciate the effort to improve JavaScript, and I'm open to further discussion on how best to achieve that. And keep in mind, TC39 will likely be as rigorous, if not more, when evaluating your proposal.
... 4 years ago
What about ??=
? How would we do something like
let a = undefined;
a ??= fn();
with ?=
? Would it be ???=
?
a ??= try fn()
looks better than a ???= fn()
.
UPD: Question is how the two syntaxes coexist?
??=
already exists as nullish-coallescing assignment
I think youāre talking about the same thing. Question is how the two syntaxes coexist?
What about
??=
? How would we do something likelet a = undefined; a ??= fn();
with
?=
? Would it be???=
?a ??= try fn()
looks better thana ???= fn()
.UPD: Question is how the two syntaxes coexist?
That's one of the problems with introducing an operator. If the problem is a matter of language syntax, or flow control, or semantics, an operator is not necessarily the solution; in fact, it probably isn't the solution. It's no coincidence that people here liked the try
variant.
#4
may look too close to conditional operator.
const [error, data] = mightFail() ?
"1":
"2";
Please dont' forget ASI in JS.
lol, poll should consider "none of the above" option. this is enforcing your views rather than asking people's opinions.
This is a nice pattern that I like. I have implemented a clean library to achieve it; you can check it out here: https://github.com/lacherogwu/safe-try
This syntax is better and allows all cases. The operator syntax is very limited.
const data = try await fetch("http://localhost");
const error = catch await fetch("http://localhost");
const [data, error] = trycatch await fetch("http://localhost");
const data = try JSON.parse("{}");
const error = catch JSON.parse("{}");
const [data, error] = trycatch JSON.parse("{}");
The problem with returning an array is type checking with TypeScript, you can't check if data is defined using the error value.
const process = () => "text"
const [data, error] = trycatch process();
if (!error && typeof result.data === "string") console.log(data.length);
Using objects, problems disappear.
const result = trycatch process();
if (!result.error) console.log(data.length);
But it is a longer syntax...
const { data } = trycatch process();
const { error } = trycatch process();
const { error: processError } = trycatch process();
const { data: processData } = trycatch process();
2.
try
(asthrow
)const [error, data] = try mightFail(); const [error, data] = try await mightFail();
const result: Result | null = try mightFail();
Even forcing to collecting the error using! at the end of the expression:
const result: Result | Error = try mightFail()!;
Indicating fallback expression
const result: Result = try mightFail() ?? getDefault();
//or
const result: Result | false = try mightFail() ?? false;
Or three of them
const result: Result | Error | false = try mightFail()! ?? false;
Nice idea!
Although adding a !
at the end was very criticized and makes handling the error much more forgettable.
Also, Result | Error
will be very hard to distinguish between error and result
It doesn't make sense to split them into 2 variables. Instead you can check the typeof or instance.
const result: Result | Error = try mightFail() ?? false;
if(result instanceof Error) handleError(error);
return result;
My approach it's just to collect the a result from try-catch.
It might be easier combined with future pattern matching in Javascript> (in my mind... š)
return match(try await mightFail())
{
instanceof Error -> handleError,
_ -> render
}
@pedro-gilmora that doesnāt work, because you can throw any value, including null or undefined, so instanceof (which isnāt a reliable check anyways) doesnāt help you.
@pedro-gilmora that doesnāt work, because you can throw any value, including null or undefined, so instanceof (which isnāt a reliable check anyways) doesnāt help you.
Knowing the nature of the "error", or rejections in promises case, it's up to the dev in the same way to handle it in the proper way. I see the same meaning of response handling unless you can inlight me.
I would comprehend your point of you were in a type-safe lang where the object strictly should meet the type matching requirement. I see it like that. I work with C# and I wouldn't propose such thing since there are not type union. Otherwise I would propose the same thing. I believe more in the reference-hold nature rather than splitting responses in order to make "readable" the code whereas JS already serves as pseudo type-union supporter thanks to Typescript.
@rockedpanda proposed in #45 this new pattern for safe assignment:
Promise.prototype.result = function() { return this.then(x=>[null,x], err=>[err,null]); };
For synchronous functions, a polyfill like this should work:
Function.prototype.result = function (...args) {
try {
const ret = this.apply(this, args);
return [null, ret];
} catch (err) {
return [err];
}
};
If you liked to get a result tuple, then you would call it like this:
const [error, jsonObj] = JSON.parse.result('{"key": "value"}');
Enhanced polyfill to handle synchronous functions that returns Promises:
Function.prototype.result = function (...args) {
try {
const ret = this.apply(this, args);
if (ret instanceof Promise) {
return ret.then(val => [null, val], err => [err]); // return Promise, always resolves to result tuple [error, value]
}
return [null, ret];
} catch (err) {
return [err];
}
};
then if you liked to get a result tuple, you can call a asynchronous function like this:
const [error, res] = await fetch.result("https://example.org");
It is very intentional that you can't synchronously get at the value of a promise, so result
isn't viable on Promise, and it does not make sense to add anything to functions.
That is why I'm not getting the value :), just wrapping the value in a result tuple inside .then
callback and returning the promise. You still have to await the promise after calling fetch.result(...)
@otaxhu And that is the same as a wrapper function. And that's correct because you use control structures and abstract the try catch.
The problem with this proposal is that try to solve a control flow problem, with an assignment of values. Assignment is one-way. Branches like throw are two-way, i.e. two distinct blocks of code, or a jump of instructions.
try
{
proposalSafeAssignmentOperator();
}
catch (e) {
console.log(e); // ConceptError
}
In fact, any function you make that abstracts away from the try-catch at one point, will be because there is a try-catch at another point. So a safe word to assign is to give the interpreter the job of branching behind the scenes.
[!IMPORTANT] The conceptual error I'm pointing out is precisely that flow control needs to be explicit and separate from assignment, since an assignment by definition does not have multiple paths. Trying to combine both in one operation is like trying to bend a straight line: conceptually they are incompatible.
If you want to improve error catching, or make it more readable you have to improve the language to improve flow control.
If you look at Rust and Go you will see that they are not safe assignments but a kind of implicit branching, because you are always forced to consider both possibilities (success or failure). This branching is managed by the type system (in Rust) or by error handling conventions (in Go).
Back to JS, the following code works right now:
const mightFail = () => 123;
try { var result = mightFail() } catch (e) { var error = e }
console.log(result); // 123
console.log(error); // undefined
Or:
const mightFail = () => { throw new Error ('Failed!') };
try { var result = mightFail() } catch (e) { var error = e }
console.log(result); // undefined
console.log(error); // Error
In the end, the idea is to improve the catch. That's why I proposed something that corresponded with the current language and its principles (as part of a much larger proposal):
{ var result = mightFail() } catch (error);
console.log(result);
console.log(error);
And you would get the same result. Simple to distinguish between error and result. Or, if you want you can ignore the error:
{ var jsonObj = JSON.parse('{"key": "value"}') } catch;
if (!jsonObj) console.log("It is not a valid JSON");
And with this variant you can do any throw
of any value or any kind of if
.
This is good:
const [err, res] = try foo()
const [err, res] = await try bar()
And this is bad:
const [err, res] = try await bar()
Because, consider this:
const [err, res] = try (await bar())
This should have the same effect as the above, but the proposal suggests not.
In JS, await bar()
is already an expression that returns the result of the promise, let's not change its behavior.
function _try(fn: any, ...args: any[]) {
if (typeof fn === "function") {
try {
return _try((fn as (...args: any[]) => any).apply(void 0, args));
} catch (err) {
return [err, undefined];
}
}
let returns = fn;
if (typeof returns?.then === "function") {
return Promise.resolve(returns)
.then((value: any) => [null, value])
.catch((err: unknown) => [err, undefined]);
} else {
return [null, returns];
}
}
const [err, res] = try foo(1, 2)
// becomes
const [err, res] = _try(foo, 1, 2)
const [err, res] = await try bar(1, 2)
// becomes
const [err, res] = await _try(bar, 1, 2)
// or
const [err, res] = await _try(bar(1, 2))
I've been using this polyfill for a long time, it's within my JavaScript extension library JsExt as well.
await try bar()
seems obvious when seeing it for async functions.
I often do something similar to, where undefined is my "error" state thats ignored:
const value = await bar().catch(() => undefined)
if (value?.something) {
// ...
}
This feels close to that, this would then allow you to store the promise and not yet await for the result:
const promise = try bar()
which becomes
const promise = _try(bar)
or
const promise = bar().then(result => [undefined, result]).catch(reason => [reason])
then, without need for try syntax elsewhere:
const [reason, result] = await promise;
Noting this would be a very specific case that would then be valid separately
try (await bar())()
where bar
is
async function bar() {
return function foo() {
throw "reason";
}
}
Agreed, I had originally
~=
but when it came to the function calls it was very clear it was "something different" and I dropped the equal sign, here is a comparison:const { value } ~ something const { value } ~ await promise const { value } ~ action() const { value } ~ await action() const { value } ~= something const { value } ~= await promise const { value } ~= action() const { value } ~= await action()
Both are reasonable. It feels obvious though that something else is happening here. This is not assignment until destructing right? Unless there is some way to assign this intermediate representation, which, actually makes sense too
const settled ~= await action(); if (settled.status === "fulfilled") { console.log(settled.value); } else { console.log(settled.reason); }
~=
is "not equals" in LUA š¤£
And this is bad:
await using
was chosen based on popularity, the same we might be able to assume for try await
or await try
. try await
feels more natural to: "try and await"
And this is bad:
await using
was chosen based on popularity, the same we might be able to assume fortry await
orawait try
.try await
feels more natural to: "try and await"
await using
is a statement and doesn't change the behavior of await value
expression, but try await
, according to this proposal, will.
In JavaScript, expressions execute in the order of right to left, let's not forget about that.
@ljharb because you can throw any value, including null or undefined
Which coincidentally also means that an error-value tuple used in the following manner:
const [error, value] = try await mightFail();
if (error) { /* ... */ }
is inherently insufficient and flawed.
You can throw anything - including null
or undefined
.
If the result is [undefined, undefined]
was it an error? Or was it a success?
The original try expressions proposal handled this better by returning an object with an explicit caught
boolean.
E.g.
const { caught, error, value } = try await mightFail();
if (caught) { /* ... */ }
@fabiancook
await try bar()
seems obvious when seeing it for async functions.
No. Expressions go right-to-left, so it should be try await bar()
- i.e. try (await bar())
.
You're awaiting the completion of the promise returned by the async bar
function and the result of that expression is something you want to wrap in a try expression, such that it maps to an error-value tuple -- or rather; a caught-error-value object -- to cover where the await operation throws.
@rjgotten Right! From left to right:
const res = await mightFail() catch err;
[!IMPORTANT] Results are expected; errors are caught.
So
const res = await mightFail(); // no catch
const res = await mightFail() catch; // ignore errors
const res = await mightFail() catch err; // catch error
const res = { await mightFail() } catch err; // catch error inside block
@rjgotten Right! From left to right:
const res = await mightFail() catch err;
[!IMPORTANT]
Results are expected; errors are caught.
So
const res = await mightFail(); // no catch const res = await mightFail() catch; // ignore errors const res = await mightFail() catch err; // catch error const res = { await mightFail() } catch err; // catch error inside block
You've missed the point of this proposal
Creating this issue just as a space to collate and continue the discussion/suggestions for the preferred syntax that was initiated on Twitter.
These options were firstly presented on Twitter here:
1.
?=
(as!=
)I generally agreed with this comment on the current proposed syntax:
2.
try
(asthrow
)Alternative suggestion for the await case from Twitter here
3.
try
(asusing
)4.
?
(as!
in TypeScript)š Click here to vote š
Please feel free to share any other suggestions or considerations :)