A Result class for functions that may return errors

# Josh Tumath (7 years ago)

At the moment, when we call a synchronous function that runs some kind of operation, there are two obvious ways of returning whether there has been an error: returning a boolean or throwing an error.

Returning a boolean to show whether the operation worked is simple, but doesn't give any indication as to why an operation may have failed (if it's not obvious). For example:

if (!bigComplicatedDataStore.add(someObject)) {
    console.log('There has been an error.')
}

On the other end of the spectrum, throwing errors allows us to be much more verbose about the error. However, there are overheads associated with this, and many good practices discourage using throwable errors. Additionally, as JavaScript is dynamically typed, the try..catch syntax will inherently catch every type of error; not specifically the errors we want to catch.

try {
    bigComplicatedDataStore.add(someObject);
} catch (error) {
    console.log('Oh no! ' + error.message);
}

I’m inspired by languages like Rust in how they have tried to reach a middle ground between these two methods of error handling, where a detailed object or enum is returned that includes either the correct result or information about the error. This is a simple way to return error results without the overheads of throwing Errors.

Rust has two enums for this purpose: Option and Result (doc.rust-lang.org/book/error-handling.html#the-option-type). The additional benefit of using this method for handling errors in Rust is that, at compile time, the Rust compiler will force you to handle the error. While JavaScript would not be able to do that, there would still be an advantage to having some kind of Result object in the standard library. For example:

function add(data) {
    if (isNotAnObject(data)) {
        return new Result('error', new Error('The data is not an object.'));
    }

    return new Result('success');
}

const result = add(someObject);
if (result.success) {
    console.log('Hooray!');
}

if (result.error) {
    console.log(result.error.message);
}

My design of the Result object isn't great, but it's just an example of how this could work. This allows us to be verbose in explaining our errors, certain about where the error originates from and leaves us without a large overhead.

Encapsulating error handling in an object, like in the examples above, enables possibilities of introducing new syntax in the future for handling such errors. (This was the case with Promises, which was simplified when the new async..await syntax was introduced.)

# Oriol Bugzilla (7 years ago)

Not sure if I'm missing something, but wouldn't it be trivial to code that constructor in JS?

class Result {
  constructor (type, value=true) {
    this[type] = value;
  }
}
function add(data) {
  if (data !== Object(data)) {
    return new Result('error', new Error('The data is not an object.'));
  }
  return new Result('success');
}
console.log(add({}).success); // true
console.log(add(12).error.message); // 'The data is not an object.'
# Josh Tumath (7 years ago)

Not sure if I'm missing something, but wouldn't it be trivial to code that constructor in JS?

Yes it would be trivial, but my design I came up with was an example. The point I wanted to get across was to have some sort of standard practice for error handling using return rather than throw, akin to how Promise gives us a standard design and encapsulation for asynchronous code.

# Isiah Meadows (7 years ago)

I would also like to point out that V8 is one of few that don't optimize try-catch, and both Chakra and SpiderMonkey do optimize them IIRC (the latter has trouble optimizing functions that throw, though).

# Bruno Jouhier (7 years ago)

try/catch is often misunderstood as people think that they MUST catch errors as close as possible to the point where they may be thrown.

Good EH practice is exactly the opposite: place a few try/catch in strategic places where you can report errors and recover from them; and let errors bubble up (without any EH code) everywhere else. With this kind of approach, you have very lean code (not polluted by error handling logic) and you keep the exception path separate from the normal execution path. This makes it easy to review how errors are handled.

And try/finally is your friend when it comes to releasing resources and restoring program invariants.

I don't see a need for a special Return class.

# Isiah Meadows (7 years ago)

I agree with this: if a result may fail normally, I would just return a sentinel value like undefined (I usually avoid null). If it's truly exceptional, don't catch it except to log it/etc.

# Josh Tumath (7 years ago)

The issue with that, though, is returning undefined is really no different to returning null in a sense. Null is considered bad because errors can crop up from not handling null as a return value from a function. Using undefined does not solve the problem. It also isn’t able to explain the failure if there could be multiple reasons.

But in cases where we do want to identify the reason for a failure and it’s not necessarily exceptional, ES does not currently provide a standard way to handle such an instance. Over the years, I’ve gone for using try..catch anyway in such instances, but many of my colleagues would disagree.

# Isiah Meadows (7 years ago)

Inline

On Tue, Oct 18, 2016, 18:23 Josh Tumbath <josh at joshtumath.uk> wrote:

The issue with that, though, is returning undefined is really no different to returning null in a sense. Null is considered bad because errors can crop up from not handling null as a return value from a function. Using undefined does not solve the problem. It also isn’t able to explain the failure if there could be multiple reasons.

That's just dynamic languages and duck typing for you - if you don't return the right type or verify what you get, it'll gladly let you shoot yourself in the foot. (The classic TypeError: undefined is not a function is much easier to debug than getting a superclass instance when I wanted the subclass one, though.)

But in cases where we do want to identify the reason for a failure and it’s not necessarily exceptional, ES does not currently provide a standard way to handle such an instance. Over the years, I’ve gone for using try..catch anyway in such instances, but many of my colleagues would disagree.

I probably suspect you're a fan of TypeScript (which now has support for nullable types) or some other language with static typing. As for getting the reason of failure, that's when I usually roll my own abstraction (usually an object with a boolean + value). Several functional languages and libraries have an Either or equivalent, which is nice, but it's a conceptually simple thing to write if the language doesn't support it.

Partially off topic, but my main use case was catching a possible exception (that the user might throw unintentionally, but my end has to be defined), in which I just created a boolean + value object literal to pass around. No need for a new constructor. That's why I'm not sure how useful it could be as a language addition.