Vadim Anisimov
Vadim Anisimov

Reputation: 121

Try/catch blocks with asynchronous JavaScript with callback functions

I have a piece of code:

function backgroundReadFile(url, callback) {
  var req = new XMLHttpRequest();
  req.open("GET", url, true);
  req.addEventListener("load", function() {
    if (req.status < 400)
      callback(req.responseText);
  });
  req.send(null);
}
try {
  backgroundReadFile("example/data.txt", function(text) {
    if (text != "expected")
      throw new Error("That was unexpected");
  });
} catch (e) {
  console.log("Hello from the catch block");
}

In my console I get

Error: That was unexpected (line 13)

which is just fine. But, I am told that:

In the code, the exception will not be caught because the call to backgroundReadFile returns immediately. Control then leaves the try block, and the function it was given won’t be called until later.

The question is: why other errors will not be caught here? When we have, say, connection problems, or the file does not exist? As far as I can see, the callback function won`t execute if

req.addEventListener("load") 

is not triggered, for example. But it still does - I still get the same error - Error: That was unexpected (line 13).

What does it mean - "exception will not be caught because the call to backgroundReadFile returns immediately"?

Thank you.

Upvotes: 1

Views: 1064

Answers (2)

M&#225;t&#233; Safranka
M&#225;t&#233; Safranka

Reputation: 4116

Here's a step-by-step breakdown of what happens in your code.

  1. The code enters the try-catch block.
  2. backgroundReadFile is called, with two parameters: "example/data.txt", and an anonymous function.
  3. backgroundReadFile creates an AJAX request and calls send(). Here's where the concept of asynchrony comes into play: the actual HTTP request is not sent right away, but rather placed in a queue to be executed as soon as the browser has finished running whatever code it is running at the moment (i.e. your try-ctach block).
  4. backgroundReadFile has thus finished. Execution returns to the try-catch block.
  5. No exceptions were encountered, so the catch block is skipped.
  6. The code containing the try-catch block has finished execution. Now the browser can proceed to execute the first asynchronous operation in the queue, which is your AJAX request.
  7. The HTTP request is sent, and once a response is received, the onload event handler is triggered -- regardless of what the response was (i.e. success or error).
  8. The anonymous function you passed to backgroundReadFile is called as part of the onload event handler, and throws an Error. However, as you can see now, your code is not inside the try-catch block any more, so it's not caught.

TL;DR: The function that throws the Error is defined inside the try-catch block, but executed outside it.

Also, error handling in AJAX requests has two sides: connection errors and server-side errors. Connection errors can be a request timeout or some other random error that may occur while sending the request; these can be handled in the ontimeout and onerror event handlers, respectively. However, if the HTTP request makes it to the server, and a response is received, then as far as the XMLHttpRequest is concerned, the request was successful. It's up to you to check, for example, the status property of the XMLHttpRequest (which contains the HTTP response code, e.g. 200 for "OK", 404 for "not found", etc.), and decide if it counts as successful or not.

Upvotes: 2

T.J. Crowder
T.J. Crowder

Reputation: 1074258

Your backgroundReadFile function has two parts: A synchronous part, and an asynchronous part:

function backgroundReadFile(url, callback) {
  var req = new XMLHttpRequest();             // Synchronous
  req.open("GET", url, true);                 // Synchronous
  req.addEventListener("load", function() {   // Synchronous
    if (req.status < 400)                     // *A*synchronous
      callback(req.responseText);             // *A*synchronous
  });
  req.send(null);                             // Synchronous
}

You're quite right that an error thrown by the synchronous part of that function would be caught by your try/catch around the call to it.

An error in the asynchronous part will not by caught by that try/catch, because as someone told you, by then the flow of control has already moved on.

So it could be perfectly reasonable to have a try/catch around the call to that function, if it may throw from its synchronous code.


Side note: If you're going to use the callback style, you should always call back so the code using your function knows the process has completed. One style of doing that is to pass an err argument to the callback as the first argument, using null if there was no error, and then any data as a second argument (this is often called the "Node.js callback style"). But in modern environments, a better option would be to use a Promise. You can do that with minimal changes like this:

function backgroundReadFile(url) {
  return new Promsie(function(resolve, reject) {
    var req = new XMLHttpRequest();
    req.open("GET", url, true);
    req.addEventListener("load", function() {
      if (req.status < 400) {
        resolve(req.responseText);
      } else {
        reject(new Error({status: req.status}));
    });
    req.addEventListener("error", reject);
    req.send(null);
  });
}

...which you use like this:

backgroundReadFile(url)
    .then(function(text) {
        // Use the text
    })
    .catch(function(err) {
        // Handle error
    });

But in the specific case of XMLHttpRequest, you could use fetch instead, which already provides you with a promise:

function backgroundReadFile(url) {
  return fetch(url).then(response => {
      if (!response.ok) {
          throw new Error({status: response.status});
      }
      return response.text();
  });
}

Upvotes: 2

Related Questions