arthurfiorette / proposal-safe-assignment-operator

Draft for ECMAScript Error Safe Assignment Operator
https://arthur.run/proposal-safe-assignment-operator/
MIT License
1.41k stars 14 forks source link

Alternative Approach: Implementing Safe Assignment Without ?= Operator #24

Closed rafageist closed 2 months ago

rafageist commented 2 months ago

Hi everyone,

First of all, I appreciate the effort and thought that has gone into the proposal for the ?= operator. It's clear that the intention is to simplify and standardize error handling in JavaScript, which is an important goal.

However, I believe that this proposal might not be necessary given that we can achieve similar results using existing JavaScript features. Specifically, we can handle errors and return results in a concise and readable way by using simple wrapper functions. Here's an example of how this can be done:

const synco = (operation) => {
   let result = null;
   let error = null;

   try {
      result = operation(); 
   } catch(err) {
      error = err;
   }

   return [error, result];
};

const asynco = async (operation) => {
   let result = null;
   let error = null;

   try {
      result = await operation();
   } catch(err) {
      error = err;
   }

   return [error, result];
};

Moreover, if we use our imagination, we can extend these functions to return not just tuples, but also objects or any other robust structures that might suit the specific needs of a given application. This flexibility allows developers to tailor their error handling to the exact requirements of their projects, without being constrained to a single approach. For example, we could return an object with additional metadata, logs, or context about the error and the operation, providing a more comprehensive solution.

Sync example

// Operation to be executed synchronously
const parseJson = () => {
   const data = JSON.parse('{"key": "value"}'); // Example of a potentially error-prone operation
   return data.key;
};

// Execute the operation using synco
const [error, result] = synco(parseJson);

if (error) {
   console.error('An error occurred:', error.message);
} else {
   console.log('Result:', result); // Output: "Result: value"
}

Async example

// Operation to be executed asynchronously
const fetchData = async () => {
   const response = await fetch('https://api.example.com/data');
   const data = await response.json();
   return data.key;
};

// Execute the operation using asynco
const [error, result] = await asynco(fetchData);

if (error) {
   console.error('An error occurred:', error.message);
} else {
   console.log('Result:', result); // Output: the value of data.key if successful
}

Key Points:

Conclusion:

While the ?= operator is an interesting idea, it's important to consider whether introducing a new operator is justified when we can already accomplish the same goals with existing language features. The above functions provide a simple, readable, and effective way to handle errors without adding complexity to the language.

I'd love to hear thoughts from the community on this approach and whether it might be a sufficient alternative to the proposed operator.

Thanks for considering my input!

anacierdem commented 2 months ago

Also see #9

rafageist commented 2 months ago

Also see #9

@anacierdem I reviewed the discussion in issue #9 before this, which effectively addresses potential problems with the ?= operator and explores alternative solutions. While that discussion focuses on the risks and possible workarounds, my issue emphasizes that we can achieve the same goals with current JavaScript features, without needing any new additions to the language. If the intent is to simplify error handling, we already have robust patterns available that don't introduce extra complexity.

rafageist commented 2 months ago

@arthurfiorette

Important note:

While the idea of using an assignment operator like ?= for error handling is intriguing, I believe error management should be handled at a higher level than what the interpreter or compiler directly manages. Typically, assignment operators are focused on straightforward value assignment, whereas error handling involves more complex logic. Keeping these responsibilities separate helps maintain clarity and avoid overcomplicating the language. By placing error handling in its own layer, we can maintain cleaner, more maintainable code without burdening basic language constructs.

arthurfiorette commented 2 months ago

By placing error handling in its own layer, we can maintain cleaner, more maintainable code without burdening basic language constructs.

A lot of words that don't make sense without a specific context.

There are a lot of languages with operators related to errors, Rust's ? is a example of that. Also, the try operator won the syntax contest.

khaosdoctor commented 2 months ago

As it was said before in several other issues, I don't think that the fact that something can be achieved with current constructs should be a blocker for something to work better with a new construct. Several things can be achieved just using JS, array/object grouping, set operators (intersect, union, etc) but still, they've made in as proposals.

IMHO, the whole goal of a proposal is to improve on existing syntax, so things we do often should be improved on by creating constructs around the language that make those things more ergonomic.

rafageist commented 2 months ago

@arthurfiorette @khaosdoctor

The TC39 technical committee will carry out a rigorous analysis. I leave you with aspects that I believe they will review: #33

TheUniqueMathemagician commented 2 months ago

I’ll join @khaosdoctor on this. Just because a feature already exists doesn’t mean we can’t try to improve it. Before async/await, there were Promises, which were more difficult to understand than the new syntax. Now everyone uses async/await and, I agree, sometimes still has to fall back to the old Promise model in some rare cases to handle old API callback patterns. That said, the new syntax is much more maintainable and faster. With this proposal, we can write in one line what previously took several lines.

Even though try/catch makes the errors more visible than a simple ?: operator, it has a flawed design in terms of scoping. This often forces developers to write the whole code inside the try block because it depends on a value declared within it, which could throw something else, so the try catch is more like a global "please, prevent my code from crashing in production", than a real tool as it was designed for.

try {
    const result = await fetch(...);

    /* logic here that could throw something else */
} catch {
    /* handle more than one thrown error */
}

Sometimes, it becomes even worse in terms of readability, as people start to:

rafageist commented 2 months ago

@TheUniqueMathemagician @arthurfiorette @anacierdem @khaosdoctor

You will always need to:

What options do you have for this?

The Promise catch still exists and is a method, just like if you use a wrapper function. The concepts of function and method exist and can be reused. The current try-catch works for both async and sync issues.

So, it is a misconception that "safe assignment" #32 is safe. Assignment is always safe. And I think it is too much responsibility for an operator.

The first question that the TC39 committee asks, and it is a requirement that they ask you, is if what you are trying to add can already be solved with what already exists. According to the rules:

Ideation and exploration. Define a problem space in which the committee and the champions can focus their efforts.
- Make the case for an improvement
- Describe the shape of some possible solutions
- Identify potential challenges
- Research how the problem is dealt with using available facilities today
- Research how the problem has been solved by other languages or in the library ecosystem

When you start to let your imagination fly, many alternatives appear (complying with ECMA):


// 1. inline solution

try let result = await fetch(...) catch err
try result = await fetch(...) catch err
try result = await fetch(...)
let result = await fetch(..) catch err

// ...

// 2. catch like switch-case
try:
// ...
catch err:
 // ...
finally:  // (or finally; / break; to finish...)
// ...

// 3. catch for any ECMA blocks (anonymous,, functions, cases, classes, if, ...)

{   ....   } catch (err) {   } finally {   ...   }

function foo() {  } catch (err) {  } finally {   ...   }

if (...) {   } catch {   } finally {   ...   }

class Person { ..... } catch (err) { ... } finally {   ...   } // if something happens inside the class

.... I want to go back inside the block!!!

But when you think about it you end up asking yourself: is it worth it? Would it be better to continue using the current try catch?

You can also think about improving the current try-catch. I've thought about it myself and still question myself every day, whether it's worth it or whether to continue using a switch inside the catch: https://github.com/rafageist/proposal-multiple-catch-when

In the end, what is there is probably more than enough and there is no need to create problems where there are none. The traditional try-catch is a proven solution. An assignment operator should not have the responsibility of handling exceptions. If you want a weird solution that is semantically understood as "safe assignment", do something like this and you won't need an if:

catch(response = await fetch(...)) { 
  // response is an exception here
  // ... return!
}

// MAYBE response is not an exception here

The challenge I see is that if you don't get out with a return in the catch, you always have to ask down below if there was an error or not.

When you review all those alternatives and find problems you end up concluding that try-catch already solves it.

khaosdoctor commented 2 months ago

In the end, what is there is probably more than enough and there is no need to create problems where there are none. The traditional try-catch is a proven solution.

You could argue that callbacks are also a proven solution, and yet, we have promises, which are also another proven solution with then/catch, and yet, we have async/await. We can go even further, assembly is a proven solution, yet we have C (another proven solution), yet we have CPP, Go, Rust, and JavaScript. If proven solutions moved the world, we would be discussing C headings here on our SVN servers from our Telex machines.

Clinging on the idea that "if it works leave the way it is" is really not what drives innovation, and certainly not (at least for me) the way the JS community has been behaving since forever. We have multiple examples of "good enough" solutions that were replaced by incredible solutions (jQuery, MooTools, Lodash, Underscore), much of those not only inspired but were literally copied into the spec. There's always room for improvement, even in proven solutions.

Add another way to catch exceptions, for which people will wonder why there are 2 ways.

But here's the thing, there won't be two ways. If we look at #4, using Symbol.result would be indeed a problem since previous functions like JSON.parse would need to be updated to include that symbol, however, transforming the operator into a syntax sugar like async/await, you won't create a new way to handle errors, you will hide the current way behind a curtain.

People can use both of them the same way Node has fs and fs/promises, the same way a third of JS's APIs uses callbacks (setTimeout, setInterval) and the other third doesn't (like fetch, async iterators...) and the final third is event-based (streams, mouse events, XHRHttpRequest, etc).

Do you wonder about why are there three completely different paradigms to handle data in JS very often? I really don't, and I don't believe other users will do so because of try/catch (which is already considered a bad pattern).

do something like this and you won't need an if

So the problem is the if? Because try/catch already works as branching, it's the closest thing to an if/else we have besides switch... That solution you proposed is not bad at all for me tbh, the problem is that it touches way more stuff than just a single part of the language, we are talking about expressions and other stuff... It's way more complex to implement that, and thus, to make a case for it...

Your points are valid, but they're not strong enough for me to justify ditching this improvement. Especially since other languages like Go already implement the same pattern and are proven solutions

rafageist commented 2 months ago

In the end, what is there is probably more than enough and there is no need to create problems where there are none. The traditional try-catch is a proven solution.

@khaosdoctor

I was just giving some additional considerations. But my arguments from the beginning are 2:

  1. The current language allows to achieve the same thing without the need for an operator (this issue)

  2. An assignment operator should not have the responsibility of flow control. The most that has been achieved is conditional assignment. An exception is a branch in the program flow (issue #33)

Both arguments will be raised by the TC39 committee. For this reason I thought of valid language alternatives such as control structures, not assignment operators.

khaosdoctor commented 2 months ago

Yes I see your point and it’s totally valid! I’m not saying you’re wrong, and it’s important to bring those points here so we can handle those.

But both of those points, in my opinion, are weak to justify the rejection of this proposal. Not only because of all the points I mentioned both here and on #33 but also because the current state of the spec already has this in place, and there are plenty of previous art that were implemented in the spec. And these issues are a good discussion to prove that

rafageist commented 2 months ago

I have created an issue #37 dedicated to the critical point in question. I am closing this one because I believe everything has been addressed and it is up to the developers and TC39 to resolve it.