Try-catch oneliner

@claudiameadows My two cents on why const [error, result] = try ... is better:

Using {caught, value} makes it easy for people to write bad code because you can't know what value is really going to be:

const {value: user} = try getUser()

if (user) {
  console.log('user name is ' + user.name)
}

The code looks it could be sensible for someone who hasn't carefully read the docs, or just forgets. And it will work most of the time, so it's very possible it'll make it through a code review and a test suite. But when an error is thrown, we're going to print user name is undefined instead of doing something sensible.

The destructuring approach also makes it more awkward when you don't care about the error. Here's an example where I want to load the eslint module, and get the ESLint class from it, if it exists (it will be undefined for old versions of eslint).

With the tuple approach:

const [_err, ESLint] = try require('eslint').ESLint

if (ESLint) {
  const linter = new ESLint()
  console.log(linter.lintText(...))
}

With a destructured object:

const {caught, value} = try require('eslint').ESLint

if (!caught) {
  const ESLint = value.ESLint
  if (ESLint) {
    const linter = new ESLint()
    console.log(linter.lintText(...))
  }
}

In this case, I don't care about the error, I just care if I can get a value for ESLint, but the object form forces me to deal with the caught variable.


As for the throw undefined scenario, I agree with @coderaiser that it's an edge-case and it should be treated as such.

The try getUser() syntax could automatically wrap throw undefined with throw Object.assign(throw Error('falsy value thrown!'), { value: undefined }). If people don't like that and want to stick with their weird falsy errors, they can just not use the syntax. They shouldn't drag the rest of us who want nothing to do with falsy errors down with them.


It's worth noting, libraries like fp-ts manage this by not using an overloaded value property on their Either type, they use { _tag: 'Left', left: {...} } and { _tag: 'Right', right: {...} }.

2 Likes

So, try would work differently based on what follows? :thinking:

try <Expression> - will be try expression
try {<StatementList>} - existing try statement

So, this would be a rather new precedent in the language where same keyword works differently based on what follows next ?

what would try { a: someReallyLongComplexFunctionThatRequiresLotsOfParsing } produce? is that an object with an "a" key in a try expression, or an "a" label, inside a try statement block?

Maybe using round bracket to disambiguate the expression form (like we do for assignment object destructuring)?

That means try { will always be considered the statement form, so user can "solve" that with try ({a:1}) (or some other expression form).

1 Like

what would try { a: someReallyLongComplexFunctionThatRequiresLotsOfParsing } produce? is that an object with an "a" key in a try expression, or an "a" label, inside a try statement block?

This is a very good point, actually. OK, there is a couple ways of solving such situation:

Operator try* or *try can be introduced (similar to generators):

const [error, data] = try* JSON.parse('xx');

Maybe using round bracket to disambiguate the expression form (like we do for assignment object destructuring)?

try can be used as function similar to dynamic import

const [error, data] = try(JSON.parse, 'xxx');

functional approach, not as obvious as previous variant.

A new keyword aim (or attempt) can be introduced:

const [error, data] = aim JSON.parse('xxx');

But it can break some existing code.
I think first example is the most straight forward, for await it can look like:

const [error, data] = try* await fetch(url);

Form can be discussed, but using try as expression can be very useful, and simplify code a lot in a similar to throw expressions way.

1 Like

Update for my proposal: I changed the value member to error for caught exceptions. So try expr is now sugar for the following:

try {
    return {caught: false, value: expr}
} catch (e) {
    return {caught: true, error: e}
}
2 Likes

I really appreciate your comment, but just for the record:

for (const x of aim JSON.parse("xxx")) {
  console.log(x); 
};

// undefined (I guess?)
// JSON parse error

This would be a valid code, right?

I feel like using an Elixir/Haskell-like tuple, in that case, would be slightly better. Unfortunately, the stage 2 tuple proposal (GitHub - tc39/proposal-record-tuple: ECMAScript proposal for the Record and Tuple value types. | Stage 2: it will change!) won't solve the issue as tuples are iterable (and it would also cause a dependency on another proposal).

Even using objects won't solve the problem as we can iterate its keys.

Maybe this is not a problem at all; I'd just find it odd to see code looping over an array used as a "tuple" :slightly_smiling_face:

I have a couple ideas.

TryExpression

Block Statement can be passed to try expression similar to module blocks

const [error, result] = try {
    await JSON.parse();
    await sendData();
}

Or it can be a function with arguments listed one by one like in Function.call:

const [error, result] = await try(JSON.parse, data);
  • :white_check_mark: when code throws you have error filled with exception;
  • :white_check_mark: when result returns, error has value null;

:point_up:Cases like throw undefined and result = undefined is impractical and should be mentioned in the spec.

Try Expression in ESTree can look like:

{
    type: ‘TryExpression’,
    await: false,
    body: {
        type: ‘tryBody’,
        name: {
            type: ‘Identifier’,
            name: ‘parse’
        },
        arguments: []
    }
}

For code:

try(parse);

In other cases body can contain BlockStatement

Function.prototype.try

Other variant that is easily pollified is:

const [error, data] = JSON.parse.try(data);
const [error, data] = await JSON.parse.try(data);

ooh, I like the Function.prototype.try() idea. Functions often have a better chance of getting through than syntax does, so we might be able to get further with that. It might work even better as a static method though, so that it can easily be used against arrow functions, for example:

const [error, result] = Function.try(() => JSON.parse(data))
const [error2, result2] = Function.try(() => {
  // ...a bigger block of code...
})
1 Like

About static blocks, would be great to avoid creating inner functions, since this is same syntax overhead as regular try-catch blocks, that gives nothing in return.

This varian would be perfect:

const [error, result] = Function.try(JSON.parse, data);

The only thing: it’s pretty long, and cannot be shorthand to try since it’s reserved.

The idea of having a tuple of error and result is where I think the most pushback would happen. node-style errorbacks are largely discouraged in favor of promises, and "go does it" doesn't hold much water.

I mostly want this to have an expression form of try-catch. Something like this would make me happy as well:

const result = Function.try(() => {
  return JSON.parse(x)
}).handleError(err => {
  return null
})

I'm not overly tied to the tuple-style that's currently presented. I also know that you're toying around with a pattern-matching try-catch thing once the pattern-matching proposal is done, which, if that goes through, I'll be completely satisfied.

I guess do-blocks would satisfy this craving as well. So, there's a number of options out there on the horizon.

node-style errorbacks are largely discouraged in favor of promises

Callbacks are discouraged, not [error, result] tuple, I mentioned a lot of userland libraries that has such return value.

Promises is OK, try-catch monstrous blocks is not.

So much noice that gives no sense:

const result = Function.try(() => {
  return JSON.parse(x)
}).handleError(err => {
  return null
})

Would be amazing to avoid all this inner functions and make things in declarative way.

That's not actually a lot more noise if you look at the bigger picture (i.e. if we include the logic that actually handles the errors, of which, there should always be logic like this).

Compare these two:

// With the tuple
function openConfigFile() {
  const [error, result] = Function.try(() => {
    return fs.readFileSync(CONF_PATH, 'utf-8')
  })
  if (error.code = 'ENOENT') return null
  if (error) throw error
  return result
}

// Without the tuple
function openConfigFile() {
  return Function.try(() => {
    return fs.readFileSync(CONF_PATH, 'utf-8')
  }).handleError(err => {
    if (error.code = 'ENOENT') return null
    throw error
  })
}

All that's really going on is we're moving the logic to actually handle the error value into the handleError() callback. It also has a handful of other advantages, like, if you consider the case where you use multiple Function.try()s in the same function:

  • You don't have to find a bunch of unique names to give to your different error objects
  • You don't have to pollute your scope with a bunch of extra error variables (I like keeping the number of variables in the current scope lower)
  • The error handling logic is bunched together in one place, so if you're just trying to follow the "happy path", it's easy to skip over it as you scan the code.

That being said, I don't have strong opinions on either solution. I was mostly throwing this out as a rough alternative that doesn't use tuples if it's decided we need to go a different route.

I can't agree that tuples code not cleaner, check it out:

function openConfigFile() {
  const [error, result] = Function.try(fs.readFileSync, CONF_PATH, 'utf-8');
  
  if (error.code ===  'ENOENT')
      return null;

  if (error)
      throw error;

   return result;
}
  • :white_check_mark: no lots of braces;
  • :white_check_mark: all code can be read from top to bottom line-by-line;
  • :white_check_mark: everything is going on in one scope, so no need to jump here and there;

But yes your one scope will be polluted and yes uniq names should be figured out, but that's definitely for good!

The error handling logic is bunched together in one place, so if you're just trying to follow the "happy path", it's easy to skip over it as you scan the code.

Much simpler to scan code without any nesting, returns, scopes, indents and much-much more of freedom inner functions gives you.

If it was ever to be implemented, I think there will be expectation to follow Promise.allSettled behavior, with {status: "fulfilled", value: 99} and {status: "rejected", reason: Error: an error}

1 Like

one possible benefit of having syntax instead of a utility function is that it could 'just work' in async/await:

const {status, value, reason} = try await request();

Instead of:

const {status, value, reason} = (await Promise.allSettled([request()]))[0];

or with a 2nd promise based helper:

const {status, value, reason} = await Function.tryAsync(() => request());

In this case, when you need a couple functions to run in try-catch you need to rename arguments:

const {status: status1, value: value1, reason: reasone1} = try await request();
const {status: status2, value: value2, reason: reasone2} = try await request();

That's why array destructuring is more comfortable.

one possible benefit of having syntax instead of a utility function is that it could 'just work' in async/await

Exectly! Without this libraries can always be used:

And they also just works, but should be installed/bundled/imported independently on each environment again and again.

One edge-case downside to arrays is when the error value is undefined, as it would look like a success:

const [err, value] = try await Promise.reject();
typeof err; // 'undefined'
typeof value; // 'undefined;
2 Likes

const [err, value] = try await Promise.reject();
typeof err; // 'undefined'
typeof value; // 'undefined;

Indeed, but what a use case for this? I have never had a need to throw undefined.

const [err, value] = try await readFile(name, ‘utf8’);

In what case both error and value will be undefined?