Try-catch oneliner

I haven't either. But, you might not be dealing with your own code, you might be dealing with silly code that other people write. For example, if your library is calling a passed-in callback and you're wanting to know if it throws.

No use cases but I have seen it happen, usually unintentional e.g. 'return Promise.reject()' or 'throw obj.typo'. It can be important to consider these edge cases for core parts of the language due to how widely they can get used, it increases the chance they will encounter these cases.

Re-emphasing the word 'consider' to try and be clear :slightly_smiling_face: Potentially the array design adds up to be the better one. Working out the best design is about weighing the upsides against the downsides.

There is always an option to return a third item in the array that informs you whether or not it was an error or a result.

const [err, result, wasError] = Function.try(...)

That way, if you don't care about the third value, you can just ignore it.

// Remaining elements are automatically ignored
const [err, result] = Function.try(...)

Another thing to remember, is that you don't have to destructure the values at that spot if we're going the object route. This is currently my prefered way of working with this sort of system:

const maybeUserId = Promise.try(() => createUser(...))
if (maybeUserId.status === 'ERROR') { /* handle error */ }
const userId = maybeUserId.value
/* Continue the "happy path" */

This makes it easier to deal with the scenario that you might be using Promise.try() multiple times in the same function.

1 Like

A couple thoughts to add here. The static method idea is a great idea, given the try keyword is reserved without it, especially since this prospective method is already shaping up to look a bit like the native Fetch API.

A couple different directions we can go here, and some of these points are purposely reiterated—

  • The Tuple Approach

    Tuple destructuring certainly is growing in popularity among frameworks and otherwise. Personally, I prefer the more promise-y "fetch…then"-style approach to this implementation, which I think would also seems more familiar to most users.

    If the tuple approach were to be implemented, I strongly think result should be the first destructured value, as error handling is often optional, even in native try…catch blocks. Destructuring result-first would uphold the same order of significance.

  • A New Method

    If a new method were to be implemented for this inline try…catch implementation, I think it could be implemented similarly to the style of the Fetch API (window.fetch). This is how that could look, with the catch and finally blocks being entirely optional:

    Function.try(() => {
      // some logic...
    })
      .catch(error => console.error(error.message))
      .finally(() => console.log('try…catch…finally completed'));
    

My vote is for the latter. Many of the other conventions mentioned here are possible as well, though I'd argue many of them are very counter-native in their structure and could be awfully confusing to most devs.

I haven't either. But, you might not be dealing with your own code, you might be dealing with silly code that other people write.

Just use regular try…catch for that sort of things, I don’t suggest to remove constructions that already exists but we should consider that language develops in a way of good practices, we have:

  • :white_check_mark: strict mode to report errors early;
  • :white_check_mark: const to support block scoping and avoid redeclarations;
  • :white_check_mark: import and export to have ability determine dependencies before running the code;
  • and much more!

I think we must move in direction of not silly, but high quality code and describe all edge cases in spec.

const [err, result, wasError] = Function.try(...)

I like this idea, wasError gives ability to determine all the edge cases and don’t stand on the road and can be skipped in most cases.

If the tuple approach were to be implemented, I strongly think result should be the first destructured value, as error handling is often optional, even in native try…catch blocks.

Error should always be checked, because it’s significant part of evaluation, if you take a look at node-callbacks, and all the fs methods, you will see that it definitely should be handled. Anyways, if you don’t need it, just skip:

const [, data] = await try(readFile, ‘utf8’);

100% agree here. It's a bad practice to catch an error and ignore it, and I would never want a language feature that encorages this practice. In fact, I would want to make the error the first item in the tuple just to remind people how important it is to use that error value.

If you expect a specific error to be thrown, then check for that specific error and rethrow the error if it's not the specific type. If you're not sure what errors you expect to be thrown, then figure it out. If you just ignore the error, then how would you ever know if there was a bug in the try portion of your code? You're catching and ignoring all of the errors, including helpful information to track down real bugs.

That's certainly a possibility. I was actually trying to avoid giving it the same callbacks that you would find on a promise, because I didn't want to make this look like it was doing async logic when it's not, but it's possible having a familiar API is more important then giving it the appearance of a async logic. It's something that can be bikeshedded on.

1 Like

This is very close to what I proposed here: GitHub - dead-claudia/proposal-try-expression: A proposal to introduce `try` expressions into ECMAScript

It's been on their radar in some capacity for a while, though I haven't actively pushed for it (yet).

The only case I can find about throw null is Javascript puzzler: throw null – niksilver.com. But I cannot reproduce this, I have a TypeError. But that articles gives a nice idea from Java: when null was thrown it throws NullPointerException. So what we can do instead of catching an object with a property wasError is: always return an Error object this way:

function tryCatch(fn, args) {
    try {
        return [null, fn(...args)];
    } catch (error) {
        return [error || Error(error), null];
    }
}

In this case error will always have an object, even if it was null or undefined:

const fn = () => throw null;
const [error, result] = try fn(a);

if (error)
    console.error(error);
    // Error: null
    //     at <anonymous>:1:1

The benefit of such way of handling things is:

  • each variable takes control over one entity: error or result;
  • no need of any additional property, like wasError;
  • no need to use destructuring renaming:
const {error: userError, result: userResult} = try getUser();

So the wolves will be full and the sheep will be safe.

That's not a benefit. The language allows throwing any value, including null and undefined, and everything else around exceptions must do the same.

1 Like

The language allows throwing any value, including null and undefined, and everything else around exceptions must do the same.

For what purpose it must be the same? Isn’t it is the same case as with var and const, block scoping and other benefits of strict mode. Nothing should be the same, developer must not have ability to shoot in it’s feet. Show me the reasons to throw undefined and null when it will be useful.

1 Like

I throw (or promise-reject) null and undefined often, and find it very useful - they are both explicitly “no value” signals in many domains, and it’s much clearer to me at times than creating an entire error object with a stack trace unnecessarily.

1 Like

I throw (or promise-reject) null and undefined often, and find it very useful - they are both explicitly “no value” signals in many domains, and it’s much clearer to me at times than creating an entire error object with a stack trace unnecessarily.

I cannot imaging where it can be clear, but if you take a look at errors in node js API, you want see any null or undefined. Same with Deno. We have error object which can have error code to determine what exactly happens.

await readFile(‘/not exist’);
// throws with error.code === `ENOENT`

Show me the code where it necessary and cannot be avoided to throw null. And even more: To catch null. It’s not clean at all, and I have only one such case, and problem was in my code, so I fixed it and forget because no matter what you throw, you should always catch an error with message and other fields you need, this is a good practice and this is what language provide us (MDN).

Putting “whether throwing null is useful or not” to one side. It does seem valuable for a try-expression to follow the same semantics of a try-catch statement. So moving from one to the other is not a refactoring hazard.

Also, as previously shown, it's not that hard to just include some sort of isThisAnError boolean in the return value, so people who actually care about it can use it. People who don't care don't have to touch it, and their code will look exactly the same as if we didn't support it.

For example, if the try function returns a three-tuple, with the third item being the isThisAnError boolean, and you don't care about it, you can just destructure it like this:

const [err, result] = Function.try(...)

And, if it's returning an object that contains an isThisAnError property, you can just destructure it like this:

const { err, result } = Function.try(...)

While, those who actually want to make sure they deal with code that might be throwing null or undefined (remember, this could be other people's code doing it, not your own), they can support it, by simply extracting out that boolean flag while destructuring:

const [err, result, isErr] = Function.try(...)
// or...
const { err, result, isErr } = Function.try(...)

Is there something about this solution you don't like?

I'm absolutely good with this solution, for people who need to relate on such stuff third argument will be very useful, other's can drop it, the only thing I think that Array tuple is better because in one function can be lots of errors and:

// is too long:
const { err: firstError, result:f irstResult, isErr: firstIsError} = Function.try(...)

// and that's much better who read code and most of the time we read code
const [firstError, firstResult, firstIsErr] = Function.try(...)
1 Like

I occasionally use non-Error rejections and exceptions for control flow, as it's easier than using an optional/maybe or choice/either type when conditions may be nested particularly deep within code and the plumbing with all the conditionals would otherwise get extremely boilerplatey in a hurry.

Just implemented an extension to acorn, that supports constructions like:

const [error, result] = safe hello();

and

const [error, result] = safe await hello();

Actually there is (almost) no problem to use try instead of safe:

const [error, result] = try await hello();

but that was faster for now.

So it's really possible, and pretty simple. About a question: what if an object will be passed, I can tell you that this have no sense, and should be forbidden. That's it :).

Would be amazing if you guys try it, and share the feedback, or create an issue with any ideas you have about extending JavaScript. We try to implement it, and this will give us much more information about ergonomics of a new language constructions.

I guess there's a tried and tested solution from Rust that might work as an inline/expression level error handling mechanism;

async function myFunction() {
    const file = await? readFile("my-file.txt", "utf-8")
    console.log(file) 
}

here if readFile() rejects/throws an error myFunction() stops execution and returns the error object. This feature can thus only work inside of functions.

I chose to go with await? instead of try? because it can be used with Promise/non Promise based objects.

try? await myFunction()

The above syntax works but await? is much more concise. Share your thoughts!

Hi, I don't see the benefit.

The existing await operator already does that.

I don't understand, it seems like await? can be used only with promise objects? An approach that works with non-asynchronous expressions, like the operators proposed in the previous comments, would be much preferable. There also is a clear semantic difference between try myFunction() and try await myFunction() that should be the same as the difference between myFunction() and await myFunction().

Going back to the original suggestion, what if it worked more like this:

let value = try? doSomething() : handleError(e);

Expanded out, this would look like:

let value;
try {
   value = doSomething();
} catch (e) {
   value = handleError(e);
}

In essense, its exactly the same structure as the ?: except that it's a try/catch. I would even like to go one step further and allow for finally as well:

let value = try? doSomething() : handleError(e) :: cleanup();

For this, at least one of the catch part (: statement) and the finally part (:: statement) must be present. I think this arrangement would handle all of the wants that have been mentioned over the past few years regarding this. In combination with await, it could even manage async work.

let value = try? await doSomething() : handleError(e) :: cleanup();

Since each section of this exception ternary can contain a statement, await can be applied where needed. Since a rejected await becomes an exception, no new mechanics are needed. Given that having any keyword preceed the ternary operator is currently an error there is no syntax issue that I can think of. So what's the opinion on this version of it?