DEV Community

Discussion on: Callback hell OR try catch hell (tower of terror)

Collapse
 
peerreynders profile image
peerreynders

Your main function would look something like this

The thing is if you present code written in the manner described you have to expect to raise some eyebrows because you are ignoring error codes. Obviously you felt the need to do this - however work arounds like this are symptomatic of poor error design.

Defensive programming is messy

So messy in fact that the designers of Erlang invented "Let it Crash" (Programming Erlang 2e, p.88):

Many languages say you should use defensive programming and check the arguments to all functions. In Erlang, defensive programming is built-in. You should describe the behavior of functions only for valid input arguments; all other arguments will cause internal errors that are automatically detected. You should never return values when a function is called with invalid arguments. You should always raise an exception. This rule is called “Let it crash.”

However most runtimes don't have the luxury of letting the process die and having the supervisor deal with the crash (p.199):

If this process dies, we might be in deep trouble since no other process can help. For this reason, sequential languages have concentrated on the prevention of failure and an emphasis on defensive programming.

However there is another point to be made - not all errors are equal. Roughly speaking:

  • Expected errors. Errors will occur routinely during the operation of the software and therefore should be handled appropriately.
  • Exceptional errors. Errors that indicate that fundamental assumptions about the software and the environment that it is operating in have been violated. These type of errors cannot be handled at the local scope, so local processing is terminated and the error is passed upwards repeatedly until some kind of sensible compensating action can be taken.

Not all languages have exceptions but they have idioms for exceptional errors. Golang:

    // some processing
    result, err := doSomething()
    if err != nil {
        return err
    }

    // more processing ...
Enter fullscreen mode Exit fullscreen mode

Rust has the error propagation ? operator.

  let mut f = File::open("username.txt")?;
Enter fullscreen mode Exit fullscreen mode

i.e. for Ok(value) the value is is bound to the variable while an Error(error) is returned right then and there.

When a language like JavaScript supports exceptions the rule of thumb tends to be:

  • Use error values for expected errors.
  • Use exceptions for unexpected, exceptional errors.

So when we see

const [areas, areasErr] = await getAreas();
Enter fullscreen mode Exit fullscreen mode

areasErr is an expected error and should be handled, not ignored. And just because an error code is returned doesn't necessarily imply that getAreas() can't be a source of unexpected errors. When we see

const areas  = await getAreas();
Enter fullscreen mode Exit fullscreen mode

the code is implying that there aren't any expected errors to be handled locally but getAreas() can still be a source of unexpected errors.

With this in mind - 4.1.3. Rejections must be used for exceptional situations:

Bad uses of rejections include: When a value is asked for asynchronously and is not found.

i.e. a promise should resolve to an error value for expected errors rather than rejecting with the expected error. So when we see

const [areas, areasErr] = await promiseResolver(getAreas);
Enter fullscreen mode Exit fullscreen mode

there is a bit of a code smell because all errors deemed by getAreas() as exceptional are converted to expected errors at the call site and then are promptly ignored. There is an impedance mismatch between how getAreas() categorizes certain errors and how the code using it treats them. If you have no control over getAreas() then an explicit anti-corruption function (or an entire module for a "layer") may be called for to reorganize the error categorization (and the associated semantics), e.g. :

function myGetAreas() {
  try {
    return await getAreas();
  } catch (err) {
    if (ERROR_NO_ACTION_NAMES.includes(err.name)) return [];
    else throw err;
  }
}  
Enter fullscreen mode Exit fullscreen mode

so that the consuming code can be plainly

const areas = await myGetAreas();
Enter fullscreen mode Exit fullscreen mode

Compared to the above

const [areas, _areasErr] = await promiseResolver(getAreas);
Enter fullscreen mode Exit fullscreen mode

comes across as expedient (though noisy) and perhaps receiving less thought than it deserves.