DEV Community

Cover image for A plain man's guide to the Javascript fetch() API and the "await" keyword
MartinJ
MartinJ

Posted on

A plain man's guide to the Javascript fetch() API and the "await" keyword

Introduction

The Fetch() api provides a neat way to enable browser-based Javascript code to communicate with server-based database. It's particularly handy when you need to deliver a dependant series of such calls. If you're still using XMLHttp requests (XMRs) for this purpose, you may find this post worth reading.

In my humble IT hacker existence, I generate quite a bit of Javascript that does no more than set up and dispose of calls to the server-based PHP code managing my databases. In the past I used XMLHttp requests for these tasks but struggled with the cumbersome code that resulted. You'll doubtless know that XMRs should ideally be asynchronous. You need to keep your wits about you when such calls are inter-dependant - the code can get very long-winded and obscure. Here's an example of a simple XMR:

1  <form id="dummyform"></form>
2  <script>
3      var inputField = "input";
4      function getMyData(data) {
5          const form = document.forms.namedItem('dummyform');
6          const oData = new FormData(form);
7          oData.append("input_field", data);
8          const oReq = new XMLHttpRequest();
9          oReq.open("POST", "mydatabaseinterface.php", true);
10         oReq.onload = function (oEvent) {
11             const responseJSON = oReq.responseText;
12             const outputFieldsArray = JSON.parse(responseJSON);
13             alert('Success : output = ' +  outputFieldsArray[0].outputfield);
14         };
15         oReq.send(oData);
16     }
17     getMyData(inputField);
18 </script>
Enter fullscreen mode Exit fullscreen mode

The "engine" of the getMyData() function above can be found at line 8 where the example creates a new XMR object. Prior to this the code is concerned with preparing a FormData object to allow me to send a data field up to the server. In classic HTML code you would use standard <form> and <input> tags to do this but I've found it much easier to use a dummy form to build my own FormData objects and add the fields into this explicitly using the FormData's append method.

The asynchronous database call doesn't actually get launched until line 17 where the code deploys the XMR object's .send method. After this point, Javascript just moves on to the next job on its agenda, but prior to this the XMR object has registered an "onload" event to activate a "callback" function to receive the response from the database call. So our initial script has effectively forked and become two parallel threads of execution. The new one is temporarily halted, waiting for a response from the php module, but at some point it will signal an "onload" event. Processing will then resume and the results will become available to the initial script. As long as we bear in mind the fact that we can't foresee exactly when this will actually happen and make sure we don't try to start up any activity that will need these results until they do appear, all will be well.

The snag, however, is that what we've just seen is a rather long-winded way of setting up the asynchronous call, and when we need to nest dependant calls within their instigators things get messy. For example, if we wanted to make a second database call dependant on the first in the code above, we would need to insert this after line 13, ie within the anonymous onload function. Here we would duplicate all the code for lines 3 though to 16 (but using new variable names of course). This does not make for code that's easy to follow!

Please note that I've not included any error handling in the example, nor have I described the organisation of the php module. I'll come back to these later.

Anyway, I'm happy to say that there's a solution to these difficulties - cue roll of drums - the fetch() API and its attendant new Javascript "await" keyword. Here's the example code rewritten using fetch() and await:

1  <script>
2      var inputField = "input";
3      async function getMyData(data) {
4          const response = await fetch("mydatabaseinterface.php", {
5              method: "POST",
6              headers: { 'Content-Type': 'application/json' },
7              body: '{"input_field": "' + data + '"}'
8           });
9          const outputFieldsArray = await response.json();
10         alert('Success : output = ' + outputFieldsArray[0].outputfield);
11    }
12    getMyData(inputField);
13 </script>
Enter fullscreen mode Exit fullscreen mode

The "engine" is now provided by the "fetch" call at line 4. Note, immediately, that the arrangements for parameterising the fetch() function are a lot more workmanlike than those for XMR, consisting of just a pair of arguments to deliver, firstly, a target url and, secondly, an "init" object specifying the comms method, the headers and any input data.

Note in particular that the "form" element central to the XMLHttp method appears to have disappeared from the scene entirely. Be assured though that if you really do need a form - say to upload a file - then 'Content-Type': 'application/x-www-form-urlencoded' is available to allow you to submit a formData object as your body). See Mozilla's documentation at https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch for details of all the properties available in the init object.

The real magic of the fetch() api appears at line 4. Whereas you would normally expect Javascript to push straight on after the fetch() call launched here, the await call embedded in the statement has the effect of making it pause and wait for the asynchronous result to appear in the response variable.

Goodness me - asynchronous activity behaving synchronously in Javascript. Whatever next?

This works because the fetch() api returns its result as a "promise" rather than a simple value, and the architecture of promises, combined with the actions triggered by the "await" keyword are used by the Javascript interpreter to create the impression of synchronous code. Behind the scenes, of course, the interpreter is still dynamically creating an onload event for a callback function. This consists effectively of the lines of code that follow the fetch call. But we don't need to know about any of that! The whole point is that the complex structure of asynchronous operation so brutally exposed in the first example is now hidden and the code starts to become significantly more meaningful.

Taking this further, a whole series of asynchronous dependant database calls can now be coded as if they were synchronous:

await databasecall1;
'
'
await databasecall2;

and so on.

Note that the await command is only available in functions that have been declared with another new keyword - async.

Moving on to line 9, the script sets about dealing with the response object returned by fetch(). This is quite a complex object containing many useful properties. In particular it contains a response code to tell us whether the fetch has completed successfully (code 200) and a data item containing the text returned by the php module. As you'll see in a moment, a convenient way of structuring this is to use json format. This works well because the response object has a .json method that turns a json into an array. Note, however, that this is another asynchronous step so we have to use another await keyword to maintain the synchronous illusion. Note also that .json is only one example of numerous methods for addressing the response object - see Mozilla's documentation at https://developer.mozilla.org/en-US/docs/Web/API/Response for details

At this point I think I should say that there is more than one way of handling the promise returned by a fetch() call. The "await" keyword is a relative newcomer to standard Javascript and prior to this you would have used a promise's ".then" method to code a sequence of asynchronous processes like so:

<script>
    var inputField = "input";
    function getMyData(data) {
    fetch("mydatabaseinterface.php", {
        method: "POST",
        headers: { 'Content-Type': 'application/json' },
        body: '{"input_field": "' + data + '"}'
    })
        .then(response => response.json())
        .then(responseData => alert('Success : output = ' + responseData[0].outputField));
    }
    getMyData(inputField);
</script>
Enter fullscreen mode Exit fullscreen mode

Here the initial fetch() call returns a promise that resolves as a "response" object.This is then passed as a parameter to the succeeding ".then" method. Here a function is applied to the response in which it invokes the response's ".json" method. This returns yet another promise that resolves with the result of parsing the body text of the "response" object as JSON - a JavaScript value of datatype object, string, etc. This is given as a parameter to the next ".then" which again applies a function to dispose of it. In this particular instance, the function simply opens an "alert" window and displays the result.

Since ".then" always returns a promise, if you've got more asynchronous fetch's to launch, you just add them as further ".then" statements

In the example above, following standard practice, I've used arrow notation to code the callbacks (for example, "x => return x+2" is just a shorthand way of writing "function(x){ return x+2);}"). In more complex cases you might find it preferable to code the callback function conventionally for greater clarity.

Many developers (me included) find the modern "await" form with its "declarative" "let x= await ..." statements a lot easier to understand. I think you may have already detected my views in this regard because of the difficulties I've just displayed in trying to describe the ".then" style. Promises are a fine concept but I prefer not to see too much of them While the "await" method is underpinned by the promise architecture and its callback "success" and "fail" functions, these callbacks are no longer explicitly defined and the flow of data through the chain of awaits is perfectly clear - each line of declarative code in this case shows you exactly where the result ends up and links smoothly to the next.

That said, a lot of documentation still uses the older ".then" style so it's useful to know that the two approaches are essentially identical.

One difference however is that the ".then" approach can makes use of the native ".catch" method possessed by all promises. This can be placed at the end of a ".then" chain to pick up any exception that might occur. In the case of a "wait" stack, you have to put a try/catch block explicitly around the stack itself.

So, let's talk about error handling. Because in practice we use the word "error" in two different senses I'd like to distinguish between what I call "state" errors and what are more precisely referred to as "exceptions". By "state" errors I mean things like invalid user input where your program is still running but has realised that it has arrived in an unsatisfactory state. Because it's still running, it can take appropriate action. By contrast, when an exception occurs (something has gone irretrievably wrong and explicitly "thrown" an error), you need to provide "catch" instructions to surround the vulnerable code, otherwise your program will stop dead in its tracks.

Fortunately for us, whereas you'd have thought that the fetch() api would be a rich source of exceptions because of the tendency of the http interface to go so spectacularly wrong in so many different ways, the api actually handles all of these and just returns a general success flag (response.ok) plus error detail codes if we need them. These are "state" errors in my terminology and we deal with them using conventional "if" statements.

But there are plenty of other opportunities for exceptions to arise. A particular example that's very relevant to the example here is where the php module fails and adds an error message to its output stream. In such a case, the json structure echoed by the php module become invalid and response.json throws an exception.

Here's an error-tolerant version of the example using await:

<script>
    var inputField = "input";
    async function getMyData(data) {
        const response = await fetch("mydatabaseinterface.php", {
            method: "POST",
            headers: { 'Content-Type': 'application/json' },
            body: '{"input_field": "' + data + '"}'
        });
        if (!response.ok) {
            alert('Oops : Network response error');
            return
        }
        try {
            const outputFieldsArray = await response.json();
            alert('Success : output = ' + outputFieldsArray[0].outputField);
        } catch {
            alert("Oops : error in json");
        }
    }
    getMyData(inputField);
</script>
Enter fullscreen mode Exit fullscreen mode

It has to be admitted that adding this error-handling seriusly degrades the readability of the code so you may want to consider carefully where it needs to be deployed. For example, while it is very likely that response.json errors will be encountered during system testing, these shouldn't be seen in a production system. Conversely, while in the example above, fetch() has been considered sufficiently stable that it doesn't need to be included in the catch block, you might take a different view if you were working with other apis. Some judgement is called for here.

To conclude, I promised I'd say something about the design of a PHP module to service the request posted by a fetch() call.

As you'll recall I've used the body property in the init object to tell fetch() to POST my endpoint an input parameter coded as a json object - {"inputfield1":data}.

You might imagine that in the php module itself you would use $_POST[' '] references to retrieve them in the endpoint, however, because we're not using a url-encoded form, php's $_POST facility array isn't available. We have to do is use a
file_get_contents('php://input') command to retrieve the body property as a string. However, it's then a simple matter to parse this as a JSON and retrieve the parameter.

Getting the response back is just as easy - basically you just "echo" it. Here I'm just going to echo back the same data field that was received as input so that, if all goes well, I can display a comforting "output = input" message. Although I could have returned this just as a string (and fielded it using response.text rather than response.json), because you're more likely to want to return multiple parameters, I've packaged the return into a json.

$data = file_get_contents('php://input');
$dataAsJSONObject = json_decode($data);
$input_field = $dataAsJSONObject -> input_field;
$output = '[{"outputField""' . $input_field . '"}]';
echo $output;
Enter fullscreen mode Exit fullscreen mode

Simples!

I hope that you have found this all useful. Why not give fetch() a try now.

Discussion (0)