Error Dejection Operator

That's an interesting idea - adding a "finally pipeline operator" would complete the try-catch-finally concept. (btw - maybe something like "error catching pipeline operator" is a better name? For short, "catch pipe"?)

Though, thinking more into the details of what a pipeline operator does, I think a finally pipe wouldn't fit very well. Let's take this example:

const parsedFileContents = openFile(...)
  |> file => file.read()
  |> content => parseContent(content)

Being a good person, you want to clean up your file resource that you created in the pipe, so you want to tack something like this at the end of the pipeline:

  |< file => file.close()

But we quickly realize a couple issues:

  • How do we get our hands onto the file object? This file object is scoped to an earlier pipe that wasn't passed through the pipeline. It is possible to carry the file object through the entire pipeline along with the data we're transforming, but that adds a lot of complexity to the pipeline.
  • We don't want parsedFileContents to end up getting set to the return value of file.close(), we want it to be what was returned from parsedContent(). (it's possible to do this too, just adds a bit more complexity)

In short, the expression that goes in the finally pipe does not care about its input or output - that expression doesn't really belong as part of the pipeline, and would probably be better expressed with a normal try-finally.

1 Like

In my idea, as an error may have occur or not at this point, the handler function would not take any argument. And also, the L.H.S expression would be returned at the end, and not the returned value of the handler (if any):

expression |< handler

would desugar to something like:

(() => {
  try {
    return expression;
  } finally {
    handler();
  }
})();

Also, (just so that it is said explicitly somewhere): the "finally" |< pipeline, like the "catch" |^ pipeline, would have a lower precedence than the normal |> pipeline, allowing errors to flow inside the pipeline until they are handle by one of these operators.

But indeed, even with this behavior, the first issue you outlined still applies: how to get the object if nothing is passed as an argument of the handler?

In my example, this could be solved by assigning the stream object outside of the pipeline, like:

const stream = openStream();
stream
  |> processHeaders(?)
  |^ handleHeadesrError(?)
  |> processData(?)
  |> format(?)
  |^ handleStreamErrorSafely(?)
  |< closeStream(stream);

But then the end result is not a nice one and unique pipeline from top to bottom, which is a bit disappointing.

Also, in that case, it looks not that bad because I chose the Hack syntax for pipeline, so that I don't have to use an anonymous arrow function for the final |< pipeline.
I find that it would look a bit messier using the F# syntax:

const stream = openStream();
stream
  |> processHeaders
  |^ handleHeadesrError
  |> processData
  |> format
  |^ handleStreamErrorSafely
  |< () => closeStream(stream);

I'm not convinced the finally pipeline really shared any behavior with a normal pipeline or the catch pipeline - it doesn't take input or give output. In fact, all it really does is takes the entire expression to the left and attaches a finally block to it - which maybe could be useful as its own proposal with a different syntax. Let's say we changed the operator from "|<" to "finally":

const stream = openStream();
stream
  |> processHeaders(?)
  |^ handleHeadesrError(?)
  |> processData(?)
  |> format(?)
  |^ handleStreamErrorSafely(?)
  finally closeStream(stream);

We can also use it by its self in other scenarios

const file = openFile()
const result = processContent(file.read() finally file.close())
const stream = openStream()
const twoChars = stream.getChar() + stream.getChar() finally stream.close()

As another, possibly irrelevant note, but try-finally is my least favorite way to handle resource cleanup (but it's also the most powerful, which is why it's necessary). I like it when APIs provide an option to give it a callback that gives you the resource for the duration of a callback, then auto-clean it up afterward, it gives it a similar feel to python's "with" statement, or Java's "try-with-resource". APIs with a callback version reduce the need for a finally operator, as they're already so clean to use in simple scenarios.

openStream(stream => stream
  |> processHeaders(?)
  |^ handleHeadesrError(?)
  |> processData(?)
  |> format(?)
  |^ handleStreamErrorSafely(?)
)
const twoChars = openStream(stream => stream.getChar() + stream.getChar())

I think the catch pipeline operator needs to have the same precedence as the normal pipeline operator to work properly.

x |> f1 |^ f2 |> f3

should be interpreted as if "|>" and "|^" have the same presedence (just like "+" and "-" have the same presedence)

((x |> f1) |^ f2) |> f3

means: give x to f1, if there's an error, give that to f2, then give the result of c1 or c2 to c3.

If |^ had lower precedence (similar to how + is a lower precedence to *), then it would end up being

(x |> f1) |^ (f2 |> f3)

which would mean: give x to f1, and give f2 to f3. The result of "f2 to f3" should be a function, that we then pass the result of "x to f1" into.

Not what we wanted.

Your intentions to "allowing errors to flow inside the pipeline until they are handle by one of these operators" should already be handled, even when they have the same precedence, just like it gets handled by .then() and .catch() on promises, which also have the same precedence. One of the inner-pipeline expressions throws, and we start unwinding out of the expression until we find a catch pipeline operator, which will catch it and handle it - or rethrow it, causing us to unwind out of the expression even more.

1 Like

My bad, I'm thinking too fast and making dumb mistakes! :)
I should really be verifying twice what I'm saying before I post it here, this would save time for everyone! Thanks for correcting me! :wink:

I agree! But unfortunately it is not available on every library, that's why such a feature could be handy.

Concerning using a finally keyword, this syntax would be much closer to the other existing proposals you mentioned at the top of this discussion. But in that case, we would have a lonely finally expression, that would not have any similar catch or try expressions to go with it, and a "catch" pipeline but no "finally" pipeline to go with it either. I feel that the language would seems a little bit scattered, lacking some coherence in some way.

To make things more coherent, we could try to replace the catch pipeline |^ to a simple catch keyword (keeping the Hack syntax):

const stream = openStream();
stream
  |> processHeaders(?)
  catch handleHeadesrError(?)
  |> processData(?)
  |> format(?)
  catch handleStreamErrorSafely(?)
  finally closeStream(stream);

Things like this could be possible too:

const parsed = JSON.parse(stringified) catch ({})
const data = dangerousOperation() catch handleError(?)

While the catch keyword looks cleaner and is more descriptive about what it does when used outside a pipeline, on the other hand it looks quite out of place inside a pipeline flow. Also, it would behave in a way that would be too similar to the pipeline operator so that it doesn't actually be another pipeline operator.

After thinking (at least twice!) about it, I think you're right about the "finally" pipeline, it might seem as an interesting idea in the first place, but is actually not possible to implement in a way that would be coherent with existing (and incoming) features of the language.
I think it would be better (for now at least) to put this idea aside, an concentrate on other ideas that would be much more useful and needed by developers (and easier to implement coherently)! :)

1 Like

At first, I was a little sceptical of this idea, but I'm starting to get really excited about its possibilities. It's so much more concise than try-catch, can be used in an expression, makes it easy to create composable error handler functions, works great with the existing pipeline proposal, it's easy to understand, and does not feel out of place with other Javascript features. The "|^" syntax was a great idea (love the "up arrow" analogy).

I really hope this makes some progress.

1 Like

Thank you for your feedback, I really appreciate! :blush:

I have to admit I like this idea more than I would have thought too when I first proposed it!
I think I am going to create a github repository about this proposal soon (also, if any champion is passing by and like this idea, please feel free to let me know :wink:)

This is the continuation from a reply to theScottyJam from Statements as expressions idea.

problem with SyntaxErrors:

@/×&@¥garbage... |^ callback

This will not even parse. Therefore the error is never caught and the callback is not invoked;
normally it should follow the the behaviour of try...catch(i.e failing)
I think it should also handle SyntaxErrors? But how?? simple just keep the inverse flow that I proposed earlier.

callback |< @/×&@¥garbage...

here, callback |< part is parsed; then it encounters a SyntaxError. The error is caught and the callback is invoked.
Well that's the idea! But I don't know how the JS is parsed. Anyone's welcome to correct me here!:blush:
I seriously think that it should be kept separate from Pipeline operator Proposal.
here's how it will work then:

const y = x => (x
    |> Math.sin
    |> (x => Math.pow(x, 3))
    |> console.log
);

callback |< y(5)

Let's not mess up the work that has gone into the Pipeline Operator and keep things separate, nice and tidy.
And I think chaining it is not a good idea either. Instead we can do

call1 |< call2 |< call3 |< err3
// It's evaluated as:
call1 |< (call2 |< (call3 |< err3))
// just like an arrow function
x => y => z => "hi"

I'm now beginning to think that it shouldn't be an operator; operators cannot intercept exceptions right? It should be more like a syntax construct; a bit like arrow functions. It's rightfully an expression on its own; not exactly an infix operation involving => operator (i.e, if I get it right😉).
So that's all the new thoughts I have had!:baby:

syntax errors generally aren’t catchable (not counting ones from eval).

1 Like

yes that's what I meant by the normal behaviour of try...catch; Should this operator keep this behaviour if we can enable it to catch SyntaxErrors as well!:wink:

We can't enable that. Parsing errors can't be caught by the same runtime code that's invalid.

1 Like

so it should fail gracefully😊; well that was worth a try!

Yeah, it really doesn't make sense to attempt to catch a syntax error within the same file that the syntax error occurred, you'll be trying to execute an incomplete script! That'll lead to all sorts of issues. The best place to catch a syntax error is when you're loading the file. e.g.

try {
  const output = await import(...)
} catch (err) {
  if (err instanceof SyntaxError) {
    // ...
  } else {
    throw err
  }
}

It shouldn't be a common need either. Normally, it's pretty obvious if your program has a syntax error, and you can get it fixed before it ships to the client.


Could you expound? In what way do you not like how the "|^" operator intertwines with the pipeline operator? Any particular aspect of it that you think is confusing or bad?

Just to make sure we're on the same page, the "|^" operator isn't doing anything magical in connection with the pipeline operator. The only reason you can stick "|^" in the middle of a pipeline is because it has the same precedence, the same way you can stick a "-" in the middle of "2 + 3 - 4 + 5" and evaluate it from left to right. The linking between the two is conceptual only, they're still completely separate operators. And if we go with F#-style pipeline, the only difference between "|<" and "|^" is associativity, and maybe precedence.


@Clemdz - did you ever get around to making a proposal repo? If not, and if you have a lot on your plate, I would be happy to do so as well.

Sorry I over committed a little😅; I just thought maybe we can emulate the functionality of the Either type in Haskell to handle errors.
Haskellers do that all the time;

-- something like
main = do
    result <- try (evaluate (5 `div` 0)) :: IO (Either SomeException Int)
    case result of
        Left ex  -> putStrLn $ "Caught exception: " ++ show ex
        Right val -> putStrLn $ "The answer was: " ++ show val

see how the try function works here:
It returns an Either with Left and Right values. The Left is the exception & the Right is the value(That was all the callback |< expression syntax). This was tried and tested and I just wanted to follow it along. Maybe it won't hurt to include it along with the Pipeline Proposal. My intent as to consider it as a separate proposal was to isolate the problem we're trying to solve and to keep the focus on error handling and not worrying about other aspects connected with the Pipeline operator.
But mainly because;

Ah, now I know your inspiration, and why you chose this particular syntax.

I don't see any reason why an operator couldn't catch an exception. The operator decides how to operate on its operands, and if it chooses to catch any errors generated while an operand executes, so be it. This kind of control is similar to how the "||" operator can choose to not even evaluate its RHS operand if the LHS is falsy.

But, potatoes-potatoes. There's no solid definition of what an operator really is, and what technically counts as an operator and what doesn't.

I am somewhat familiar with haskell, and I can see better where your inspiration came from. If you're wanting something similar to haskell's try function, you may like this thread, which I would argue gets us even closer to what haskell's "try" does. Here's the proposal repo from that thread. And here's an example:

const { value, error } = try evaluate(5, 'div', 0)
if (error) {
  console.log('Caught exception: ' + String(error))
} else {
  console.log('The answer was: ' + String(value))
}

In that proposal, try becomes an operator, and it returns an either-like object. If it was successful, "value" will be set to the final value. Otherwise, "error" will be set to the error that was thrown.

Combine that with the proposed pattern-matching syntax, and we've got something that looks very similar to the haskell example you gave:

const result = try evaluate(5, 'div', 0)
match (result) {
  when ({ error }) { console.log('Caught exception: ' + String(error)) }
  when ({ value }) { console.log('The answer was: ' + String(value)) }
}
errorObject |< expression

seems like either Error or Execution.
But I thought well we're in JS and callbacks are the way to go so;

callback(errorObject) |< expression
// i.e callback |< expression

thanks for providing that snippet; I've see the shorthand try ;) but thought we can make it even ergonomic with this new construct.

there's a concept called normalisation.
I think an example can better explain it;

2 + 3 == 5

it's a simple expression. Here the terms are eagerly reduced to the smallest representation possible. Taking precedence into account: 2 + 3 is reduced to 5 then the expression becomes 5 == 5; It can be further reduced to the boolean true. It cannot be reduced further and is called the normal form. This process is called normalisation. JS is not a strongly normalising language (guessing by the lazy evaluation). Exceptions / Errors halt this normalisation process. Operators act upon expressions right? If an expression cannot be normalised how how can this process be done?
Even in lazy evaluation a part of the operation is evaluated. The cost vs the benefit to work around this is not reasonable.

No, actually I started to write a draft back then, but I didn't really had the time to do it properly, and then I left it a bit behind. So sure, go ahead and feel free to create the repo on your side! :)

1 Like

Thanks for those comments @jithujoshyjy - I'm starting to understand your inspiration better.

For now, I'm actually not going to make a proposal for this (if someone else wants to, feel free). Instead, I've recently proposed over on this thread that we make Javascript a little more expression-oriented by turning a handful of statements into expressions, including try-catch. The details about how each statement will get turned into an expression can be debated.

The proposal repo is here, and there's an issue on it here that discusses what would be the best way of providing an expression form of try-catch. It links to some different possible ideas that have been brought up in these forms, including this "|^" idea and the "const { value, error } = try whatever()" idea from the other thread. I assume only one will likely make it through, so it's just figuring out which would be the best fit.

1 Like

Seeing that repo made me think: why can't we use the existing compatible js to accomplish this?

// I mean
// if conditional
if(truthy) expression
<else if(expression) expression : optional>
<else alt_expression : optional>

// loops
for(let x = 0; x < 5; x++) expression
while(truthy) expression // maybe this is bad

//error handling
try expression
catch(e) expression
<finally expression : optional>

// for functions we already have
function(...args) {
  <expression | statement : optional>
}
// or arrow functions

These are all currently valid right (apart from try)?

Then maybe these could be made available as expressions

let exp_age = if(sam.age >  0) sam.age ** 2
// if the conditional evaluations to falsy `exp_age` becomes undefined
// or
exp_age =
    if(sam.age >  0) sam.age ** 2
    else 0

let iterator =
    for(let x of xs)
        if(x <= 0) continue
        else x / 2
// then we can iterate over it
iterator.next() // something
// or spread it to an array
let arr = [...iterator]

// now for the try!
let validate =
    try call_server()
    catch(e) handle(e)
    finally respondToServer("😝")

// validate is now whatever call_server() returns, i.e if there is no error, if an error occurs it'll be handle(e), and if that throws it will be respondToServer("😝").

Kind of looks like Haskell, sorry for that😅

Well, I know @ljharb is against adding new semantics to existing language constructs. I personally wouldn't want the behavior of things like "if" to change depending on if it's in a statement or expression position (i.e. in a statement position you can use a block, in an expression position you can't), so that's why we're currently running with a "when if"/"when try" syntax.

If we think it would be valuable to implement a for or while loop in an expression position, we could do that. I'm also skeptical about the usefulness of expression-while.