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).

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!