February 19, 2019

Don't Use Try/Catch in JavaScript I

If you're using try/catch in JavaScript you're doing something WRONG. Here's why.

Check out my previous post about when it is and is not a good idea to use try/catch if you haven't read it already.

In this article I specify acceptable uses for try/catch in JavaScript. There aren't very many. In fact, I could only think of one reason applying to Node.js, even then, it's optional. So, let's start with the abundance of reasons NOT to use try/catch, starting with the biggest no-no:

Reads/writes that need to operate over a network

Here is a bad vanilla JavaScript implementation of a network request with try/catch:

var xhttp = new XMLHttpRequest();
try{
    xhttp.onreadystatechange = function() {
        if (this.readyState === 4 && this.status === 200) {
           showResponse(xhttp.responseText);
        }
    };
    xhttp.open("GET", "filename", true);
    xhttp.send();
}
catch {
    showErrorMessage();
}

In the event of network error, the catch block will not trigger. The reason is as follows:

  1. We set "onreadystatechange" to a function, but we don't actually run the function at this step.
  2. We then open the conection and send the request. We don't wait for a response, and exit the try block without throwing exceptions, even if the user is not connected to the internet.
  3. Even with a 500 statusCode response, or no response, no exception is thrown. JavaScript does not assume you were expecting a status 200 response.

Reinterpreting the code as a timeline of events makes the problem a little more obvious:

At the end of this timeline the response was not shown... but neither was the error message. As far as the user knows, the request is still loading.

So how to handle network errors? The below is some vanilla JavaScript that handles server error responses correctly. It uses simple if statements on the readstatechange event, looking at properties exposed by the request object, and has an early abort to prevent downloading the whole response if we already know it's an error.

var xhttp = new XMLHttpRequest();

xhttp.onreadystatechange = function() {
    if(this.readyState > 1 && this.readyState < 4){
        if(this.status < 200 || this.status >== 300){
            xhttp.abort();
        }
    }
    else if (this.readyState === 4){               //when DONE
        if (this.status >== 200 && this.status <300) {  //success response
           showResponse(xhttp.responseText);
        }
        else{
           showErrorMessage();
        }
    }
};
xhttp.open("GET", "filename", true);
xhttp.timeout = 60*1000;                     //60 seconds
xhttp.ontimeout = showErrorMessage;
xhttp.send();

Fine Points

You may be wondering in the above example why we are bothering to check the status for readyState 4, seeing as we already checked the status for the earlier states and aborted if there was an error response.
The reason is that after the abort, the onreadystatechange callback will occur once more with a readyState of 4, so without checking, errors would produce the success response.


Going Non-Native

The native version of network error handling is messy and easy to get wrong. Many libraries offer easier alternatives that handle the logic. Nonetheless, try/catch is equally useless in this situation because the network request is still asynchronous. Here's an example of bad code that doesn't capture errors with a promise based scheme, such as that offered by Angular.

try{
    httpclient.get("filename").subscribe(
        function(response) {
            showResponse(response.message);
        }
    );
}
catch {
    showErrorMessage();
}

Now the correct version:

httpclient.get("filename").subscribe(
    function(response) {    //success callback
        showResponse(response.message);
    },
    showErrorMessage        //error callback
);

There are more reasons why try/catch shouldn't be used in client-side JavaScript.

Check out Part II of this article.