Error Dejection Operator

grammar:

ErrorDejectionOperator ::= Function (|< Expression)+

Hello everyone,
I intend to share an idea for a new operator in JavaScript - the Error Dejection Operator
This operator is heavily inspired by the proposed Pipeline Operator.
The Error Dejection Operator aims to bring a new error handling mechanism to JavaScript. Unlike try...catch, which can only be used as a statement, this operator enables us to handle errors within variables, function calls and other expressions.
The operator can also be used to handle Promise rejections.

const err = e => "error"

const file = err |< await fetch("file-name.json")
const json = x => ({error: x}) |< await file.json()
console.log(await json)

The operator can also be chained!

const chain = err
	|< erroneousFn3()
	|< erroneousFn2()
	|< erroneousFn1()
Here,
tries executing erroneousFn1():
	if successful returns erroneousFn1()
	else,
tries executing erroneousFn2():
	if successful returns erroneousFn2()
	else,
tries executing erroneousFn3():
	if successful returns erroneousFn3()
	else,
tries executing err(e):
	if successful returns err(e)
	else,
throws error in the current scope

the terminal function takes an error object as an argument.

1 Like

It's a nice idea. See also this related idea.

So, there are two main types of errors we usually deal with: programmer errors, and expected exceptions.
This operator does not seem like a good fit for programmer errors - these are fatal errors that should only be caught for logging/reporting purposes, so let's focus on exceptions that a user of the API may want to catch and handle in different ways.

Any time an exception is thrown, there's usually something provided to be able to distinguish one exception from another one. Some libraries will create different subclasses of Error, and you have to do an instanceof check to know which type of exception you caught. Others will set some unique property on the error object itself (for example, node sets the "code" property to "ENOENT" when throwing a file-not-found exception).

When dealing with exceptions, it's good practice to be as specific as possible in your error handling. e.g. when reading a file, if you expect that the file might not exist, catch and handle just that specific exception, and let all others continue to propagate. You wouldn't want to accidentally handle a "disk full" exception in the same way.

The current proposal has no way to cherry-pick which type of exception you're handling, instead, you're swallowing up all kinds of errors - programmer errors or non-programmer exceptions, FileNotFound or DiskFull, and treating them all the same. I fear the way this is currently proposed would encourage some bad programming practices.

Another related idea (probably more related than the first link I shared): Try Expressions

@theScottyJam sorry I forgot to mention!
the terminal function takes an error object as an argument.
Therefore there is actually a solid way to detect the error type.
i.e,

function err(e) {
   if(e instanceof SomeError)
     //do something
   else if(e instanceof SomeOtherError)
     // do something else
    else
      // handle all other errors
}
const data = err |< await fetch("file-name.json")
1 Like

Ah, didn't catch that. After thinking about this a bit, I'm really starting to see the value in this idea.

As a minor suggestion, what if it were flipped, so it went the same direction as the pipe operator (for example, if we used "!>" for the operator). This would give it a similar feel to using .then() and .catch on promises.

const HandleErrCode = (code, handler) => err => {
  if (err.code !== code) throw err
  return handler()
}

const fileContents = (
  fetchResourceSync('/some/path')
  !> handleErrCode('NotFound', () => '{}')
  !> handleErrCode('NotAvailable', () => { throw new Error('This resource is unavailable') })
  |> transform
)

// Notice the similarity with the following:
const fileContents = (
  fetchResource('/some/path')
  .catch(handleErrCode('NotFound', () => '{}'))
  .catch(handleErrCode('NotAvailable', () => { throw new Error('This resource is unavailable') }))
  .then(transform)
)

I'm not sure yet how I would actually feel about adding something like this to the language, but I do think this is a good start that can lead somewhere useful. I'm certainly am not a fan of the error-handling system we currently have in Javascript.

I just suggested that syntax, any further changes to the syntax are always welcome๐Ÿ˜Š
I'm sure that someone will associate it with the proposed pattern matching syntax.

I find the idea interesting, and indeed I think it would be a great fit with the pipeline operator.

Like @theScottyJam, I think it would be better if the error flow goes top to bottom, like for the pipeline operator, or also the.then() / .catch flow, so that it stays consistent with what we're used to in the language.

If I can put my two cents on the syntax, I think that !> is visually too similar to the |> operator, and so they would not be that easy to distinguish one from the other.

I would propose instead the |^ operator:

const fileContents = (
  fetchResourceSync('/some/path')
  |^ handleErrCode('NotFound', () => '{}')
  |^ handleErrCode('NotAvailable', () => { throw new Error('This resource is unavailable') })
  |> transform
);

What I like about this operator is that it would be very visual, we still have the | that forms a continuous pipe all the way down, but the ^ is like redirecting in the "error" direction (upwards of course, because an exception was raised :wink:), instead of the "normal" flow of execution.

3 Likes

Great suggestion.
My intend was that errors are raised in the execution of the program so they disrupt the normal flow and we have the idea that errors propagate upwards(the scope). So the operator works from bottom up. This is especially handy in async situations.

const val = e => "err" |< await something()

here, it first waits for the right hand side of the operator to complete and if it's error, executes the L.H.S. Therefore the terminal error handling function need not be async, saving some syntactic noise.
another e.g:

function errorHandler(e) {
    // maybe we can use the proposed pattern matching syntax here๐Ÿ˜‰
    if(e instanceof SomeError)
      // handle the error
    else if(e instanceof SomeOtherError)
      // handle the other error
    else
      // handle all the other errors
}


let fileContent = errorHandler
     |< fs.readFileSync("some-file1.txt")
     |< fs.readFileSync("some-file0.txt")

// or the other syntax

fileContent = errorHandler
    |^  fs.readFileSync("some-file1.txt")
    |^ fs.readFileSync("some-file0.txt")

Also please note that all errors are handled by the topmost terminal function.
It is easier to notice the error handler as it appears at the top

I'd like to discuss two things about your proposal:

  • first, the associativity of the operator
  • secondly, a defense against the top to bottom direction

(warning, long message incoming! :nerd_face:)

[ Also, after posting this message, I realized the term "associativity" was not quite the right one to describe what I meant! Please have a look to this following post ]

Discussion about associativity

(here, I'm keeping the direction from bottom up, as you proposed, so that we can easily discuss one thing at a time)

First, I find problematic that this operator does not behave the same way depending on where it is called.

Taking your example:

const chain = globalErrorHandling
  |< erroneousFn3()
  |< erroneousFn2()
  |< erroneousFn1()

we have:

  • if the L.H.S is a function: return its R.H.S, or call its L.H.S if an error was thrown
  • if the L.H.S is anything but a function: return the R.H.S, or return the L.H.S if an error was thrown

or, maybe in the way you thought about it:

  • for the first time the operator is called in a chain: return its R.H.S, or call its L.H.S if an error was thrown
  • for all the other times: return the R.H.S, or return the L.H.S if an error was thrown

I'm not aware of any other javascript operator (or in any languages) that are not associative, like the one you're proposing. I also think it would be way too confusing to be actually implemented some day in the language.

In my opinion, it would be way more consistent that the |< operator always behave in the same way, by always expecting functions:

const chain = globalErrorHandling
  |< err => erroneousFn3()
  |< err => erroneousFn2()
  |< err => erroneousFn1()

There is no longer a distinction between the first operand of the chain and the other, the operator is now fully associative, everything is treated the same way.

Also, in that way, we could even have a finer error handling all the way up.

Side note: desugaring the syntax

If you're still with me, with this new behavior, this simpler example:

const chain = errorHandler |< erronousFn();

would desugar to something like:

const chain = (() => {
  try {
    errounousFn();
  } catch (err) {
    errorHandler(err);
  }
})();

so our example:

const chain = globalErrorHandling
  |< erroneousFn3
  |< erroneousFn2
  |< erroneousFn1

would desugar to this pyramid of doom:

const chain = (() => {
  try {
    return (() => {
      try {
        return (() => {
          try {
            return (() => {
              try {
                return erroneousFn1();
              }
            })();
          } catch (err) {
            return erroneousFn2();
          }
        })();
      } catch (err) {
        return erroneousFn3();
      }
    })();
  } catch (err) {
    globalErrorHandling();
  }
})();

Discussion about the direction of the flow

I persist to think that top to bottom (and also left to right) is better in javascript. The error upwards propagation will occur during runtime, but as we write and read the code, everything will always be top to bottom.

Like when you explain in your proposal,

we first have erroneousFn1, then erroneousFn2, etc. Because this is how we understand what is going to happen, and I think the same way of thinking shoud be reflected in the shortened syntax

This makes even more sense when every operand has to be a function, as there is no distinction to be made between the first operand and the others anymore.

Also, it would allow using this operator alongside the pipeline |> operator, which would not be the case if it was still working from bottom up:

// Some function definition
const filter = () => { /* ... */ };
const map = () => { /* ... */ };
const finalizeOrSmth = () => { /* ... */ };
const handleMidwayError = () => { /* ... */ }

const value = data
  |> filter
  |> map
  |^ handleMidwayError
  |> finalizeOrSmth
  |^ () => []; // If everything fails, at least return an empty array


// This would desugar to
const value = (() => {
  try {
    return finalize((() => {
      try {
        return map(filter(data));
      } catch (err) {
        return handleMidwayError(err);
      }
    })());
  } catch (err) {
    return [];
  }
})();

I personally find this last example very elegant, but I know this is going a little different from your original idea. Also, my opinion is definitely not the only one that counts here, so do not hesitate to continue the discussion, and tell what you think about it :slightly_smiling_face:

2 Likes

Agreed - not sure how pipeline chaining would work with a backwards flow:

const value = fn1 |> fn2 |< fn3 |> fn4

What does that do??

The most perspicacious among you will have realized that "associativity" is not at all the right word to describe what I meant! :)

Javascript actually has non-associative operators, like the exponentiation ** operator:
(a ** b) ** c !== a ** (b ** c).

What I did meant is that the operator behavior changes based on where it is called, but I'm not sure there is a term to describe that. But for sure, this is not "associativity"! :)

I think the word you are looking for is operator precedence? Right/left operator association is a different but valid thing to discuss.

Elm (and other languages) have |> and <|.

Where x |> f is f(x) and f <| x is also f(x), which seems unnecessary except that it has a different precedence

a |> b <| c |> d is a |> (b <| (c |> d))

True, but there's really no reason to mix Elm's "<|" and "|>" operators in the same chain, while there's plenty of reasons to want to mix the proposed "|<" with the pipe operator "|>".

Of course, parentheses could be used to help disambiguate the situation, but not very easily when spreading the expression out over multiple lines.

associativity of an operator is a property that determines how operators of the same precedence are grouped in the absence of parentheses.
This operator is Right associative like the **.
It's associativity doesn't change when chained, instead it makes use of partial application.

whether chained or unchained the L.H.S operand must be a function, else a TypeError is thrown.

here's how it works, thanks to partial application.

// pseudo code
fun = Function
dj = |<
expr = Expression

chain = (fun dj (dj (dj expr))

/** proposed partial application syntax (sadly not applicable for operators๐Ÿ˜œ) **/

chain = (fun dj (? dj (? dj expr))

you can see this in the grammar:

However, the direction of flow can only be understood through actual implementation and might well be changed.

I discourage the use of this operator alongside the proposed Pipeline Operator as it creates unnecessary complications and syntatic oddity.

Desugaring should be the concern of polyfill libraries.The pyramidal pattern in code suggests that it can be tackled more efficiently thought tail recursion.
The js engine might actually find a potential optimisation for error handling through this operator because unlike the try...catch block which checks for errors in multiple statements and expressions, it may only have to check a single expression.

I dearly appreciate your enthusiasm and concern regarding this idea! Thanks

Thank you for your response!

However there are still some things that I don't get in your explanation (it might be me, sorry). You say that the dejection operator is Right associative, and that the L.H.S operand must be a function, else it throws a TypeError.

But your grammar:

EDO ::= Function (|< Expression)+

could allow things like:

EDO ::= Function |< Expression |< Expression |< Expression

that would then be Right associative like:

EDO ::= Function |< (Expression |< (Expression |< Expression))

But here we clearly see that the operator handles most of the time an Expression as its L.H.S operand, and not a Function.

So what's the catch?

Are you referring to your proposal operator, or to the different version I proposed? Could you elaborate a bit more why you think it would create syntactic oddity or complications? I'm actually curious to know why you would think that!

When I say that, it is not a critique against the dejection operator, but the contrary!
The syntactic sugar looks way more linear and better than the desugar version, which would be a nightmare to write or read for any programmer.

And indeed, I'm being very enthusiastic about the idea!
I can already think of some handy ways to use it, like:

const parsed = JSON.parse(strigified) |^ err => ({});


// Instead of the verbose:

let parsed;
try {
  parsed = JSON.parse(stringified);
} catch (err) {
  parsed = {};
}

// Another benefit is that `parsed` can be const in the sugared version!

Well,
an arrow function, anonymous function, IIFE, math operations, function calls, comparisons, etc. are considered as expressions. The first operand is not just an expression but specifically a function. I agree that most expressions that produce an error are (function/method) calls, it can also happen by invoking a getter, trying to get obj.undefined.prop, etc.
There's a plethora of ways we can get errors from expressions.
The magic happens due to currying and partial application.

I was referring to my initial idea.
Using it along side the proposed Pipeline Operator seems a little weird. The syntax looks like it was from a functional language like haskell(well, to an extent it is๐Ÿ˜‰). I believe JavaScript has a unique touch in it's syntax, not making things too much OO or functional. Also using it like that might trip up beginners. Usually, keeping things simple is the right decision to take.

providing default values for an error is a great use case!
I wish to do a rough implementation of this idea.
Will update soon!

I think it's more the "partial application" part that confuses me in your explanation.

Does that mean that the following (using your first syntax):

const chain = errHandler
  |< erroneousFn3()
  |< erroneousFn2()
  |< erroneousFn1()

will be transformed to:

const chain = errHandler_erroneousFn3
  |< erroneousFn2()
  |< erroneousFn1()

then to:

const chain = errHandler_erroneousFn3_erroneousFn2
  |< erroneousFn1()

and finally to:

const chain = errHandler_erroneousFn3_erroneousFn2_erroneousFn1()

is that what you mean by "partial application" on your operator?

If that the case, indeed, there is some black magic happening in there!

Maybe you could try showing me what the desugar version of your operator would look like, this would for sure help me to understand your idea!

And I do agree that using your |< operator seems very confusing when used alongside the pipeline |> operator. That why I wanted to find an alternative that would work great in this case, but also more generally, in any other case.

Programmers are not going to use your operator the way you expected in the first place. The operator has to be thoroughly designed so that it fits in the language under any circumstances.

For example, even if you discourage the use of the dejection operator alongside the pipeline operator, you can be sure that one developer will eventually try to do so. The operator has to be ready for this kind of use. More generally, it has to be ready for any kind of uses and edge cases (or at least, the most you can think of), so that its implementation stays coherent with existing features of the language, and does not make it weirder in any way.

I couldn't agree more with you on this point!

If we end up going with a |^ operator, then it should act very similar to however the pipeline operator. And the details of how the pipeline operator will be implemented are still very rough - there are a couple of competing syntaxes (see here)

So, if the F# syntax wins out (where the expression should always resolve to a function, or a runtime error happens), then |^ should follow suit:

getResource()
  |^ err => handleError(err, 4, 5)
  |> x => parseResource(x, 2, 3)
  |^ someFunction
  |> anotherFunction

And if the hack-pipes version wins out, then |^ will likewise follow suit:

getResource()
  |^ handleError(?, 4, 5)
  |> parseResource(x, 2, 3)
  |^ someFunction(?)
  |> anotherFunction(?)

(The same should be done if we end up going with "|<" - they're both pipe-like, and should both have similar syntax/semantics to the pipe operator)

aside: I guess if you were dealing with curried functions, then the pipeline operator would do a partial application in the F# version, for example:

const addNumbs = x => y => x + y
// I'm using |> here for simplicity, but the idea works with |^ as well.
const addToTwo = 2 |> addNumbs
console.log(addToTwo(3)) // 5

Just thought about it, but actually the |< operator could do a really neat "finally" pipeline alongside the |> pipeline and my proposed |^ error dejection operator variation. This would allow to call the given function whether or not there is an error in the pipeline flow.

With this "finally" pipeline, the parallel with error handling keywords try / catch / finally, and with Promises methods.then / .catch / .finally, would then be total!

Also, I find the |< operator would be very visual as a finally pipe too, as it seems to tell the flow to end by going in the opposite direction!

A complete design example for the eyes:

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