Open Not-Jayden opened 2 months ago
Worth pointing out that these aren't just different syntax options, they also already imply some differences about how they work; options 2 and 4 definitely look like expressions, e.g. something like bar(...(try foo()))
could work, option 3 would introduce a new type of variable declarator (i.e. statement-level), and for option 1, I think some further clarification on what the syntax actually means is needed.
👉 Click here to vote 👈
Please feel free to share any other suggestions or considerations :)
@Not-Jayden You've shared the results page, maybe worth to update to the voting one.
Worth pointing out that these aren't just different syntax options, they also already imply some differences about how they work; options 2 and 4 definitely look like expressions, e.g. something like
bar(...(try foo()))
could work, option 3 would introduce a new type of variable declarator (i.e. statement-level), and for option 1, I think some further clarification on what the syntax actually means is needed.
Yep great callouts.
I was curious about the try
(as using
) suggestion. I thought it might have been a mistake at first, but I guess the assumption is try
essentially always assigns the value to a const
?
I was wondering if it would make more sense as a modifier of sorts rather than a declarator, so you could choose to do try const
or try let
(or even try var
).
I can't say I'd be that concerned about confusing ?=
with ??
. On the other hand, reusing try
feels out of place. I mean, language-wise, catch
feels closer to what we're doing here. But I wouldn't use either as they introduce confusion with normal try-catch.
So far, I don't see anything better than ?=
. The ?
suggests we may not get a result back, at least. Sticking the ?
on the end reads messy to me--keeping it next to the vars assigned feels better.
If we're bikeshedding the proposed syntax here .. ?=
seems too similar to bitwise assignment operations (|= for example) and too similar to ??
and ??=
. "safe assignment" is conceptually related to neither bitwise assignment nor nullish behaviors, so I'd suggest one of the try-block variant approaches.
@ThatOneCalculator just remove "results" from the url.
why [error, data]
, not [data, error
]?
why
[error, data]
, not[data, error
]?
@zoto-ff That's addressed in the proposal. https://github.com/arthurfiorette/proposal-safe-assignment-operator#why-not-data-first
On the other hand, reusing
try
feels out of place. I mean, language-wise,catch
feels closer to what we're doing here.
Fair take. I prefer to think of it as an inline try
I also dislike using try
and catch
for the reasons previously mentioned.
However, I do like something about the last approach with ()?
. It makes logical sense if you are only changing the return value of the called expression.
However coupled with optional chaining, it could simply return undefined
, but when you reach the callable expression with ()?
, it returns [value, error]
.
The "try (as throw)" reminds me of Scala Try util so maybe it's already invented.
To the tweet: try await ...
seems to me more natural as I want to consume error+result of promise that is already settled.
Here are some examples to clarify what I was thinking
?=
assignment operatorconst returnValue ?= objectThatCouldBeUndefined?.callableExpression();
In this case, returnValue
should consistently be of type [error, value]
. The syntax ensures that even if the callable expression fails OR the object is undefined, the returnValue
will always adhere to this structure.
()?
const returnValue = objectThatCouldBeUndefined?.callableExpression()?;
Here, returnValue
could either be [error, value]
or undefined
. This depends on whether objectThatCouldBeUndefined
is indeed undefined
. If it is, optional chaining will return undefined
early; otherwise, callableExpression ()?
will be called and it will return [error, value]
.
The "try (as throw)" reminds me of Scala Try util so maybe it's already invented.
To the tweet:
try await ...
seems to me more natural as I want to consume error+result of promise that is already settled.
Agree. If you leave out await, value in [error, value] should be a promise. With try await it should be the resolved promise.
Third option limit usage of a result with using
. Or it will be using try [err, data] = await fn()
?
I think this option should be disqualified.
I have a question about entire [error, data]
structure. Are they expected to be raw returned/thrown data or wrapped objects like Promise.allSettled
returns? If wrapped objects than why need two different variables instead of one? If not then how would a programmer know whether a function has thrown if error
or data
could be undefined
, like this:
function canThrow() {
if (Math.random() > 0.5) {
throw undefined
} else {
return undefined
}
}
upd: topic is raised already https://github.com/arthurfiorette/proposal-safe-assignment-operator/issues/3#issuecomment-2292678889
To avoid confusion with try…catch
, a new keyword can be introduced:
const [res, err] = trycatch await fetch(“…”);
It’s not as aesthetically pleasing as “try”, but “trycatch” is clearer and is easy to identify when scanning through code.
To avoid confusion with
try…catch
, a new keyword can be introduced:const [res, err] = trycatch await fetch(“…”);
It’s not as aesthetically pleasing as “try”, but “trycatch” is clearer and is easy to identify when scanning through code.
I don't think that it would be confusing because the context seems pretty different to me. try catch is statement while this would be expression. try catch is followed by curly while this would not be as JS does not support block expressions... well actually if they would be supported some day, it might be confusing when I think about it. Even for parser I guess...
Alright, I think that I agree with you in the end 😄
Assuming block expressions exist, following code would mean "ignore the possible exception" (returned value [err, data] is ignored) however it's pretty confusing with classical try catch stmt. If catch/finally would not be syntactically required, parser would be helpless. Something as suggested trycatch
or whatever new keyword would be much more readable in this case.
try {
if (Math.random() > 0.5) {
throw new Error("abc");
}
doSomething();
}
To avoid confusion with
try…catch
, a new keyword can be introduced:const [res, err] = trycatch await fetch(“…”);
It’s not as aesthetically pleasing as “try”, but “trycatch” is clearer and is easy to identify when scanning through code.
I don't think that it would be confusing because the context seems pretty different to me. try catch is statement while this would be expression. try catch is followed by curly while this would not be as JS does not support block expressions... well actually if they would be supported some day, it might be confusing when I think about it. Even for parser I guess...
Alright, I think that I agree with you in the end 😄
Assuming block expressions exist, following code would mean "ignore the possible exception" (returned value [err, data] is ignored) however it's pretty confusing with classical try catch stmt. If catch/finally would not be syntactically required, parser would be helpless. Something as suggested
trycatch
or whatever new keyword would be much more readable in this case.try { if (Math.random() > 0.5) { throw new Error("abc"); } doSomething(); }
With ?=
or ()?
that wouldn't be an issue
With
?=
or()?
that wouldn't be an issue
Personally I don't like the ()?
as question mark is used in Rust but with different meaning. And lately many JS tools are getting rewritten to Rust so it might be confusing for people that use both.
Also it might be too much to wrap head around when it gets to the optional chaining. I can say that I trust you that it doesn't conflict anywhere however it seems to me that it requires a lot of thinking about the behaviour... But I might be wrong, maybe it's just needed to get used to it.
EDIT: the ?=
wouldn't have any of these problems IMO. However I think that trycatch
might be more powerful as it allows silencing the exceptions. However I believe that they should not be silenced anyway, so I don't know which is better. Just saying out loud thoughts.
Just throwing it out there, when I saw the proposal I initially thought it was for destructuring an optional iterable, kinda like the Nullish coalescing assignment but a little different (allowing the right side of an assignment to be nullish in the destructure).
const something: number[] | undefined = [1, 2, 3]; // or = undefined;
const [a, b] ?= something;
[a, b] ?= something
at first glance looks like optional destructing of a value with a Symbol.iterator
function (but yeah not a thing yet)
[error, data] ?= await promise
confuses me a lot. And we have something that would sit in this place already.
We have a prior notion of settled promises, which would fit in this space for promises specifically (mentioned here too)
const [{ status, reason, value }] = await Promise.allSettled([fetch("/")]);
// console.log({ status, reason, value });
// {status: 'fulfilled', reason: undefined, value: Response}
For promises we could have a nicer function here... Promise.settled
const { status, reason, value } = await Promise.settled(fetch("/"));
Then it brings up, is this what is wanted, but for any object
const { reason, value } [settled] something
This closes any question of this vs that first (as ordering is optional), and drops the comparison to destructing iterator values. It also allows any additional keys to be defined on that returned value, not just status
, reason
, and value
Giving an example with a symbol not yet suggested, ~
, a tilde, suggesting that the value assignment is similar to the result of the object/action/function return/whatever, but its not really the same as what you expect in runtime (I know this interacts with Bitwise NOT, but the following isn't valid syntax as is, this would be some kind of assignment only expression)
The tilde (
~
)Its freestanding form is used in modern texts mainly to indicate approximation
const { value } ~ something
Then with a promise it can be reasoned with maybe...
const { value } ~ await promise
This does force a rename instead of an iterator like destructing if you didn't want to use value
and reason
though.
Instead of Symbol.result
... could it be Symbol.settle
if it was a notion of settling a result of a settleable value
For functions, if it was to settle a function call, all has to be a single statement.
const { value } ~ action()
const { value } ~ await action()
Just throwing it out there, when I saw the proposal I initially thought it was for destructuring an optional iterable, kinda like the Nullish coalescing assignment but a little different (allowing the right side of an assignment to be nullish in the destructure).
const something: number[] | undefined = [1, 2, 3]; // or = undefined; const [a, b] ?= something;
[a, b] ?= something
at first glance looks like optional destructing of a value with aSymbol.iterator
function (but yeah not a thing yet)
[error, data] ?= await promise
confuses me a lot. And we have something that would sit in this place already.We have a prior notion of settled promises, which would fit in this space for promises specifically
const [{ status, reason, value }] = await Promise.allSettled([fetch("/")]); // console.log({ status, reason, value }); // {status: 'fulfilled', reason: undefined, value: Response}
For promises we could have a nicer function here...
Promise.settled
const { status, reason, value } = await Promise.settled(fetch("/"));
Then it brings up, is this what is wanted, but for any object
const { reason, value } [settled]= something
This closes any question of this vs that first (as ordering is optional), and drops the comparison to destructing iterator values. It also allows any additional keys to be defined on that returned value, not just
status
,reason
, andvalue
Giving an example with a symbol not yet suggested,
~
, a tilde, suggesting that the value assignment is similar to the result of the object/action/function return/whatever, but its not really the same as what you expect in runtime (I know this interacts with Bitwise NOT, but the following isn't valid syntax as is, this would be some kind of assignment only expression)const { value } ~ something
Then with a promise it can be reasoned with maybe...
const { value } ~ await promise
This does force a rename instead of an iterator like destructing if you didn't want to use
value
andreason
though.Instead of
Symbol.result
... could it beSymbol.settle
if it was a notion of settling a result of a settleable value
For functions, if it was to settle a function call, all has to be a single statement.
const { value } ~ action()
const { value } ~ await action()
Outtakes
```typescript const { value, reason } settle something ``` ```typescript const { value, reason } settle await promise ``` ... Or just cause I wrote the word "maybe" earlier ```typescript const { value, reason } maybe something ``` ```typescript const { value, reason } maybe await promise ```
Without =
equal sign at all it makes it hard to see that it's an assignment.
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);
}
Well maybe it should't be an assignment at all. What if I want to pass it directly as parameter?
doSomethingWithResult([some keyword] await f())
const settled ~= await action();
doSomethingWithResult(settled)
const settled ~= await action(); doSomethingWithResult(settled)
Obviously, but keyword would allow me do it directly.
(Comments too quick, had edited to include but will shift down 😄)
But if just a single expression
doSomethingWithResult(~= await action())
Or the reduced
doSomethingWithResult(~ await action())
doSomethingWithResult(~ syncAction())
This shows where =
equals could be dropped, then if its dropped in one place, drop it throughout.
(Comments too quick, had edited to include but will shift down 😄)
But if just a single expression
doSomethingWithResult(~= await action())
Or the reduced
doSomethingWithResult(~ await action())
doSomethingWithResult(~ syncAction())
This shows where
=
equals could be dropped, then if its dropped in one place, drop it throughout.
Possibly, but if I understand correctly all the behaviour, then all these would be possible despite doing the same:
const result ~= action();
const result = ~= action();
const result = ~ action()
It seems to me that ~= works as both binary and unary operator. I think that ~=
should be strictly binary (assignment + error catching) and ~
should be unary (however ~ already exists as binary not as you mentioned and whitespace makes no difference).
Definitely interesting. Maybe in that case the try or trycatch keyword would be better?
Without the equals, as a unary operator only, it would be turning the value to its right into a settled object.
Where assignment or destructing happens could then be outside of the problem space for the operator.
const { value } =~ something
and const { value } = ~ something
where the spaces are optional/ignored seems consistent. (Note this is swapped around in characters compared to earlier comments)
const { value } = try something
const { reason } = try await promise
const settled = try something
doSomething(try something)
doSomething(try await something)
try
seems natural as a unary
Unfortunately, the syntax is bound to be the most controversial and drawn out discussion item.
But on the other hand, the polling indicates 2/3 developers are totally fine with the unary/inline try
syntax (I guess its pretty intuitive - it doesn't feel like another "thing" to memorize):
I think this suggestion to just add try
as a static method of Function
from the previous iteration of this proposal is worth consideration. Definitely a much simpler change.
const [error, data] = Function.try(mightFail);
const [error, data] = await Function.try(mightFail);
I think what I like about option 2. Try as throw syntax Is I can imagine writing it in parenthesis (try …) or wrapping its results producing statement in parenthesis try (…) which can span multiple lines
what about this?
const data = mightFail() catch (error) {
handle(error);
return;
};
what about this?
const data = mightFail() catch (error) { handle(error); return; };
I think in that case just making the current try catch block return a value would be better.
const result = try {
return someUndefinedFunction();
} catch (error) {
return "An error occurred";
};
But this proposal is more about the concept "error as a value" like in go.
const [error, data] = ...
errors are already values, this is about more ergonomic catching. since status quo try
/catch
has many issues outlined in and outside the readme. and the error handling style in go is widely considered one of its worst parts.
nothing stops present day folks from doing return new Error
instead of throw new Error
. that would be particularly fine to do once https://github.com/tc39/proposal-is-error lands. in which case the pattern this proposal currently provides would be usurped by:
const [error, data] ?= mightFail();
if (error) {
// handle 'error'
}
// it succeeded, we can use 'data'
vs
const data = mightFail();
if (Error.isError(data)) {
// 'data' is an error
}
// mightFail succeeded 'data' is data
@nektro What is the advantage of the former over the latter here?
errors are already values, this is about more ergonomic catching. since status quo
try
/catch
has many issues outlined in and outside the readme. and the error handling style in go is widely considered one of its worst parts.nothing stops present day folks from doing
return new Error
instead ofthrow new Error
. that would be particularly fine to do once https://github.com/tc39/proposal-is-error lands. in which case the pattern this proposal currently provides would be usurped by:const [error, data] ?= mightFail(); if (error) { // handle 'error' } // it succeeded, we can use 'data'
vs
const data = mightFail(); if (Error.isError(data)) { // 'data' is an error } // mightFail succeeded 'data' is data
But the point is that with this proposal, you can universally apply it to any callable expression that might throw an error, making it more ergonomic and consistent. Also, the idea that Go’s error handling is “widely considered one of its worst parts” is debatable. There’s actually a lot of support for Go’s approach, and this proposal is also getting significant backing in the JavaScript community.
I’m definitely in favor of this proposal. It addresses a big problem I encounter in real life all the time, especially with tasks like fetching data and similar operations.
@nektro What is the advantage of the former over the latter here?
i might've worded it a bit weird before the code blocks. that was exactly my point. the latter is what i prefer and possible through a different proposal if code is changed to return error
instead of throw error
. then thrown errors become tantamount to a panic in other languages and the scoping issues of try/catch become less of an issue because unahandled exception handling becomes akin to go's recover()
but i mainly prefer the first comment i left since that doesnt require any code to be rewritten out in the ecosystem and uses an error handling pattern thats far more robust than either of those two code blocks in my latter comment
... you can universally apply it to any callable expression that might throw an error, making it more ergonomic and consistent.
My main problem with this is that once you use this new syntax in a block of code, it loses its ability to throw. Thus I don't agree that it is more ergonomic nor consistent.
Regular errors are consistent because they are always thrown and caught. Once you lift them in this new tuple realm, the consistency is broken. If there was a mechanism to maintain this property (somehow propagating these pair of values without manual intervention) it would be ok, but it just changes it in a form that is not compatible with the existing error handling mechanism.
I don't even think this is "more ergonomic catching" @nektro.
This is not related to the original discussion but it seems to be veering of.
... you can universally apply it to any callable expression that might throw an error, making it more ergonomic and consistent.
My main problem with this is that once you use this new syntax in a block of code, it loses its ability to throw. Thus I don't agree that it is more ergonomic nor consistent.
Regular errors are consistent because they are always thrown and caught. Once you lift them in this new tuple realm, the consistency is broken. If there was a mechanism to maintain this property (somehow propagating these pair of values without manual intervention) it would be ok, but it just changes it in a form that is not compatible with the existing error handling mechanism.
I don't even think this is "more ergonomic catching" @nektro.
This is not related to the original discussion but it seems to be veering of.
But that's the whole point. You catch the error. Why would that be inconsistent? It's just like putting a try block around it.
I actually rather agree with @nektro and understand this proposal as an adoption utility tbh.
Let's say I am up to returning errors but my underlying function is "exception based" so I wrap it into "trycatch" (or whatever syntax is accepted).
In other words, I don't understand why I should use exception system while it apparently does not suit my needs.
@alexanderhorner then how is this different from just returning the error?
@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 how fetch
was designed. You can't change it to return errors instead of throwing them. You can however make alternative ways to try ... catch
blocks to better handle them. This is what this proposal is about.
// Function using traditional try...catch blocks
const fetchWithTryCatchBlock = async () => {
let response;
try {
response = await fetch('https://api.example.com/data');
} catch (fetchError) {
console.error('Failed to fetch:', fetchError);
return [];
}
let json;
try {
json = await response.json();
} catch (parseError) {
console.error('Failed to parse JSON:', parseError);
return [];
}
// Return json.items or an empty array if json.items doesn't exist
return json.items || [];
};
// Function using the proposed error handling syntax
const fetchWithProposedSyntax = async () => {
// [error, data] using ?= operator
const [fetchError, response] ?= await fetch('https://api.example.com/data');
if (fetchError) {
console.error('Failed to fetch:', fetchError);
return [];
}
// [error, data]
const [parseError, json] ?= await response.json();
if (parseError) {
console.error('Failed to parse JSON:', parseError);
return [];
}
// Return json.items or an empty array if json.items doesn't exist
return json.items || [];
};
// Usage
fetchWithProposedSyntax().then(items => console.log('Fetched items:', items));
const UserPostsWithTryCatch = async ({ userId }) => {
let posts;
try {
const response = await fetch(`https://api.example.com/users/${userId}/posts`);
posts = await response.json();
} catch (error) {
// Handle the error by simply not assigning any value to posts, allowing it to remain undefined
console.error('Failed to fetch or parse posts:', error);
}
// If fetching posts failed, posts would be undefined, which the optional chaining operator `?.` will handle
return (
<div>
{posts?.map(post => (
<UserPost key={post.id} post={post} />
)) ?? <div>No posts available.</div>}
</div>
);
};
const UserPosts = async ({ userId }) => {
// Attempt to fetch user posts and ignore the error if it fails
const [error, posts] ?= await fetch(`https://api.example.com/users/${userId}/posts`)
.then(response => response.json());
// If fetching posts failed, posts would be undefined, which the optional chaining operator `?.` will handle
return (
<div>
{posts?.map(post => (
<UserPost key={post.id} post={post} />
)) ?? <div>No posts available.</div>}
</div>
);
};
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));
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));
Yes, but in that case just use the current try catch syntax and make it return something imo.
@nektro @alexanderhorner The problem I with putting error to lower scope "enforces" (or rather leads) programmer to early return and kinda blocks doing anything else with it IMHO.
@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.
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 :)