Enhanced Try Syntax

Proposal: GitHub - mohsen1/proposal-enhanced-try-syntax: This proposal suggests extending the try statement in JavaScript to allow for more flexible error handling patterns

Hi everyone,

I want to introduce a new proposal for making try statements more flexible in JavaScript.

The main ideas are:

  1. Allow try without catch or finally:
try {
  doSomethingRisky();
}
// Code continues here even if there's an error
  1. Allow try with just an expression:
let data = try JSON.parse(input) ?? {};

These changes would make it easier to handle errors in some common situations. For example:

// Try to install multiple things, continue even if some fail
try {
  tryInstallingPolyfill('feature1');
  tryInstallingPolyfill('feature2');
}
console.log("Setup finished");

// Try to delete a file, ignore if it doesn't exist
try fs.unlinkSync('temp.txt');
console.log("Cleanup done");

This idea came from earlier talks about try without catch and try-catch one-liners.

Motivation

Ignoring errors however one can feel about it is a very common programming pattern in JavaScript. See this search query as an example. There are more than 100K results in Github. I'm sure you wrote functions like:

function safeParse(input: string) {
  try {
    return JSON.parse(input)
  } catch {
    return
  }
}

const config = safeParse(inputConfigMaybeJsonString) || defaultConfig

This syntax allows for a shorter version:

const config = try JSON.parse(inputConfigMaybeJsonString) || defaultConfig

I've put more details in the GitHub repo. I'm looking for a TC39 member to champion this proposal.

What do you think? Any feedback or questions are welcome!

5 Likes

I like this one, I just wonder if this would promote silent errors all over the place once landed and usable out there ... still, I have various cases where such shortcut would make sense.

As I mentioned in the motivation section of my post, an empty catch block or catch blocks that return undefined is a very common programming pattern and it's not always bad code either.

Swift for instance has the try? keyword working like this and that's also very commonly used in iOS apps:

let jsonResult = try? parseJSON("{'key': 'value'}")
1 Like

I've been programming on really complex codebases, many of which are from apps that have millions of users today. I've never had a case where I'd want to explicitly not handle a raised error in some way.

But I'm very curious. I could be wrong in my assumptions. Can you list a real-world use case where you explicitly need to ignore (not handle) a raised error?

1 Like

@ke67 I have this section in the original post

This is a very common pattern. Programs ignore errors for various reasons. This is the same sprit of optional catch binding

Any argument for optional catch binding (or against it) can be used for this proposal as well. No having to write an empty catch block is an improvement

I'm all for enhanced try syntax. I have encountered multiple times where a try case without a catch is useful in the last year.

The code below may not make sense but the calls to read file would throw errors if the file doesn't exist. You have to use a try block to keep your code execution running but the catch block is useless because of the code before it. The try block is only necessary because the call itself throws errors.

      // make sure the output directory exists before trying to read it
      try {
        var outputFolderCreated = true;
        var outputFolderExists = fs.existsSync(outputFolder);

        if (outputFolderExists==false) {
          fs.mkdirSync(outputFolder, { recursive: true });
          outputFolderCreated = true;
        }
      } catch (error) { }

There are other cases when checking for browser features:

// this is not the exact code but it's similar if i can find it I'll post it
// set the default value but try the new value
element.setStyle("style", "safeOption");
try {
    element.setStyle("style", "newFeature");
}

Related thread: Omitting the catch block for the try expression

2 Likes

This idea seems to come up very often. Often times it's argued against it due to "encouraging bad practice" but in the real world empty catch blocks are everywhere.

And for catch expression there is a lot of code like:

let result;
try {
  result = JSON.parse(input);
} catch (e) { }
processResults(result); // processResults can handle undefined 

which can be shortened to:

processResults(try JSON.parse(input));

How can I convince a TC39 member to champion this? This is my first time proposing anything and I'm not sure how to get the attention of TC39 members.

Personally I wouldn't worry that omitting catch encourages bad practice. However I'm currently not sure that it is so common that the slightly shorter syntax is worth the cost. catch {} is explicit that the intention is to ignore all exceptions.

For expression level try, it would be good to see how GitHub - tc39/proposal-do-expressions: Proposal for `do` expressions progress, if the language had those it would mostly alleviate the need for a new try

1 Like

Got one even older: Try-catch oneliner

I feel like I even saw it come up once or twice during the ES6 discussions, though I can't remember for certain. I also believe I saw something like it on the old harmony wiki.

It's one of the older suggestions I've seen.

An example I found in my code:

export function getFileExists(path): boolean {
   var fileExists: boolean = false;

   try {
     fileExists = fs.existsSync(path);
   } catch (error) { }

   return fileExists;
}

For some reason some file system and other calls will throw an error if and when you want to do something simple like check if a file exists. Why? I don't know but they are out there. So you need to wrap that call in a try catch block but you don't need the catch block.

Also, see axios API. They throw an error if the response is not 200 or in the 200 block. Why? It's just they way they set it up. Did an error actually occur? No. Do you need the catch() block? Not always. Does that mean your code is bad?

To answer the question that keeps coming up:
Does not using a catch block mean your code is written poorly?
Absolutely not

fs.existsSync returns a boolean and doesn't throw.

2 Likes

I think you're right. There was some other file api that did throw errors. I might have updated the code and left the try catch in there. The point is though, that some apis will throw errors when you use them and you don't need the catch block.

I found it. The JS environment I was using didn't have sync support or that API. We were told to use folder.getEntry(). This is the function to check:

async function getFileExists(fileName) {
  const dataFolder = await fs.getDataFolder();
  var fileExists = false;

  try {
    const file = await dataFolder.getEntry(fileName);
    fileExists = true;
  } catch (e) {  }

  return fileExists;
}

@velara That's a perfect example of bad code, though. getFileExists() is a completely useless function, because you cannot do anything sensible with its return value.

1 Like

How is it bad code? That code to me is fine and works as expected.

import getFileExists...

// file may exist already - check if it exists before writing to it
var fileExists = getFileExists("log.txt");

if (fileExists==false) {
    // create file
}

// add to log

That code might be racing with another process, and between the getFileExists() call and your file creation code, the file might have already been created, causing your code to throw.

File creation is a situation where you need to apply RAII principles - just create the file without checking, and catch the error that occurs if it already exists.

2 Likes

It's not racing anything. It's not mario kart. It's checking if a file exists before it's created.
You're focusing on the wrong thing here. The proposal is do we want to prevent compilation if the developer omits the catch block.

Okay, it sounds like you're not understanding the problematic case there (it's ANOTHER program running at the same time, nothing to do with your code itself), but it is indeed a tangent from the topic at hand.

What Tab is talking about is a Race condition - Wikipedia. It's technical jargon. And what he means by "That code might be racing with another process" is that one of two situations could happen that could break your code in very nasty ways:

  • Your code in process A could check the file for its existence and see it exists, then your time slice ends and the OS unschedules process A. Then, the OS runs code from process B. Suppose this other code deletes the file, then the OS re-schedules your thread. As a result, your log writing code may break because the log file no longer exists.
  • Your code in process A could check the file for its existence and see it is missing, then your time slice ends and the OS unschedules process A. Then, the OS runs code from process B. Suppose this other code also creates the log file and writes stuff to it, then the OS re-schedules your thread. Now, you may have just wiped the logs the code from B wrote, and if you used anything like O_CREAT | O_EXCL to create the log file, the log creation code will also break.

I'm curious why catch block without the error binding try {} catch {} was not subject to the same argument? That too, "encourages" bad programming practices. My argument for this proposal is in two folds:

  1. There is already probably millions of instances of ignoring the error and doing nothing out there in the open source code we have access to.
  2. Other programming languages have this kind of syntax (e.g. try! in Swift) and there are actual instances that ignoring the error is valid.

Here is an example of ignoring the error that is valid in the wild:

    let db = []
    try {
      db = JSON.parse(res.body.toString())
    } catch (e) {}

There are millions of catch (e) {} on GitHub and it would be nice if JS had a shorter syntax for it.