A Result class for functions that may return errors
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.'
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.
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).
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.
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.
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.
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 returningnull
in a sense. Null is considered bad because errors can crop up from not handling null as a return value from a function. Usingundefined
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.
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
andResult
(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.)