Function.prototype.try for error handling

Rationale

By introducing Function.prototype.try, developers can manage errors in a structured and consistent format, similar to the approach seen in Go. This method allows errors to be treated as regular return values, which encourages more robust error handling practices and prevents errors from being overlooked.

Description

The proposed Function.prototype.try method encapsulates function execution within an implicit try-catch block, returning a Result object that contains the fields value, error, and an isError boolean flag. For asynchronous operations, the method returns a Promise that resolves to a Result object, ensuring that error handling remains consistent across both synchronous and asynchronous contexts.

const result = JSON.parse.try("}"); // no destructuring
const { error, value, isError } = JSON.parse.try("}"); // object destructuring
const [err, json, isErr] = await fetch.try("http://localhost/"); // array destructuring
const [err] = await doSomethingAsync(); // array destructuring

Example usage

const [fetchError, response] = await fetch.try('https://api.example.com/data');
if (fetchError) {
    console.log('Failed to fetch data:', fetchError);
    return;
}

const { value: data, isError } = await response.json.try();
if (parseResult.isError) {
    console.log('Failed to parse JSON');
    return;
}

console.log('Received data:', data);

Working (naive) implementation

class Result {
    error = undefined;
    value = undefined;
    isError = false;

    // this will allow to use both object and array destructuring
    [Symbol.iterator] = function* () {
        yield this.error;
        yield this.value;
        yield this.isError;
    }
}

Function.prototype.try = function (thisArg, ...args) {
    const result = new Result();

    try {
        const promiseOrValue = this(...args);

        if (promiseOrValue instanceof Promise) {
            return promiseOrValue
              .then(value => {
                result.value = value;
                return result;
              })
              .catch(error => {
                result.error = error;
                result.isError = true;
                return result;
              });
        }

        result.value = promiseOrValue;
        return result;
    }
    catch (error) {
        result.error = error;
        result.isError = true;
        return result;
    }
};

Source: GitHub - alex-solovev/proposal-function-try

1 Like

Related discussion:

1 Like

Hey, thanks @senocular I've read through it, seems like most of the focus was directed towards try operator. I think that a method on the Function.prototype would be easier to justify. On top of that my proposition improves on return value semantics by allowing both object/array destructuring (or none at all).

It is sad to see that the interest to this kind of feature has died out (the latest message from Delegate is 2 years old), hopefully this proposal can change that.

Nice, this could have a much lower bar for acceptance than try expression syntax.

Unfortunatelly, it wouldn't work with methods:

let res = o.m.try(x) // m called with this !== o

You'd have to use something like:

let res = o.m.bind(o).try(x)

I would also not include the isError property. I'm aware that one can throw falsy values, including undefined, but I believe that practice should be actively discouraged.

1 Like

Hey @lightmare thank you, I appreciate your feedback!

I think that binding issue could be easily solved by using thisArg argument, similar to what call, bind and apply do (as well as bunch of Array methods e.g. forEach, map, filter etc.). You would be able to call it like so:

let res = o.m.try(o, x)

The example implementation would change to:

Function.prototype.try = function (thisArg, ...args) { // 1. add thisArg
    const func = this; // this is just for readability
    const result = new Result();

    try {
        const promiseOrValue = func.call(thisArg, ...args); // 2. call function with thisArg

As for the Result.isError – I'm also not such a big fan of it, however I believe that there should be an escape hatch in case when falsy values were thrown.

One alternative to consider is a new syntax, something like

let res = o.m@.(x)

where the @.( ) operator (syntax modeled after the ?.( ) conditional invocation operator and the proposed ~.( ) bind operator, borrowing semantic meaning from PHP's @ error-suppression operator) is a modified function/method call operator that has the semantic meaning of the Function.prototype.try method you're describing, only it works equally well for method calls.

It might be that TC39 isn't inclined to use up syntactic real-estate with an operator for this purpose, but a good proposal weighs all alternatives.

1 Like

I definitely am not inclined to introduce new syntax for this purpose, but I do like the API form. I've actually suggested the same thing elsewhere, though I won't have time to champion this in the immediate future.

On the return type, I think { success: true, value } | { success: false, error } is the right API shape. Compare Promise.allSettled, which returns { status: 'fulfilled', value } | { status: 'rejected', reason } - those names are very promise-specific (and bad), but otherwise it's the same sort of thing.

On the question of handling methods, you could also have a tryCall that takes as its first argument the this, like call, so like let res = o.m.tryCall(o, x).

4 Likes

Hey @bakkot thank you for the feedback! It's nice to see that there is a Delegate who is interested in this proposal (even though you don't have time to push it through).

I like the Function.prototype.tryCall idea to accommodate for method calls.

The Result object shape you're proposing is only different in success field instead of isError and I think I can agree that it would be nicer to have the name without capitalization.
On the other side (this is just a personal preference and I would not fight for it) – I'm not a big fan of success because in order to check for the error you would need to negate it e.g.:

const { success } = JSON.parse.try("");
if (!success) {
    // handle error
}

Perhaps, catched could be an alternative name? Either way, naming is hard, so I think we should not focus on it too much just yet.

"threw" is already kind of a negation. Returned booleans should correspond to the positive condition, not the negative one. That's why we have Set.prototype.has and not Set.prototype.absent, for example. So regardless of the name, the boolean true should indicate "did not error".

1 Like

I could live with that in the world where this API would get implemented in JavaScript.

Will note that a try expr syntax also allows for try await and try yield, unlike a method form. And try await would encapsulate many of my uses. Syntax here is strictly and significantly more expressive than a method.