Asynchronous JavaScript and APIs

Lord AbiollaLord Abiolla
12 min read

Concept Overview:

Topics:

  • Synchronous and Asynchronous programming

  • Callbacks

  • Promises

  • Async / Await

  • Fetching Data from APIs

  1. Synchronous and Asynchronous Programming

In a synchronous programming model, things happen one at a time. When you call a function that performs a long running action, it returns only when the action has finished and it can return the result. This stops your program for the time the action takes.

Asynchronous model allows multiple things to happen at the same time. When you start an action, your program continues to run. When the action finished, the program is informed and get access to the result.

Let’s compare synchronous and asynchronous programming using a small example here: a program that makes two requests over the network and then combines the results.

In a synchronous environment where the request function returns after doing its work, the normal way would is to make the requests one after the other, which of course has a drawback of making the second request wait for the first to finish.

The solution to this problem in a synchronous system would be to start additional thread(another running program by the operating system, since modern computers nowadays have multiple processors), since a second thread could start the second request and then both threads wait for their results to come back.

Asynchronicity cuts both ways. It makes expressing programs that do not fit the straight-line model of control easier, but it can also make expressing programs that do follow a straight line more awkward.

  1. Callbacks

Callbacks are functions passed as arguments to other functions. The callbacks are invoked after the function they are passed to has finished its task and are used to handle the result of an asynchronous task.

Callbacks form one of the approaches to asynchronous programming.

The asynchronous function starts a process, sets things up so that the callback function is called when the process finishes, and then returns.

As an example, the setTimeout function, available in Node.js and browsers, waits a given number of milliseconds and then calls a function.

setTimeout(() => console.log("Hello World!"), 500);

In the above code, () => console.log("Hello World!") is the callback function passed to setTimeout() as an argument.

Another example of a common asynchronous operation is reading a file from a device storage. Imagine you have a function readTextFile that rads a file content as a string and passes it to a callback function.

readTextFile('shopping_list.txt', content => {
    console.log(``Shopping List: \n ${content});
});

// Shopping List:
// Peanut butter
// Bananas
  1. Promises

A slightly different way to build an asynchronous program is to have asynchronous functions return an object that represents its (future) results instead of passing around callback functions. This way, such functions actually return something meaningful, and the shape of the program closely resembles that of synchronous programs.

A promise can therefore be defined as a receipt representing a value that may not be available at the moment but provides a then method that allows you to register a function that should be called when the action for which it is waiting finishes. When the promise is resolved(meaning its value becomes available), such functions are called with the result value.

A promise can further be defined as a JavaScript Object that represents the future results of an asynchronous operation. Think of it like “I promise to give you the result... not now, but later — either success or failure.”

3.1. Why Promises

Let’s say you want to do 3 things in sequence, where each is depending on the result of the previous one. The tasks are to get data from a server using getData(), then process the data in the processData(), and finally display the data to the users using the displayData(). Using callbacks, we’ll have something like this:

getData(function(result1) {
  processData(result1, function(result2) {
    displayData(result2, function(result3) {
      console.log("Done!");
    });
  });
});

In the above code,

getData(callback) is called.

  • It starts an asynchronous operation (like a fetch or database read).

  • When it finishes, it calls your callback function and passes result1.

Inside the callback:

  • You call processData(result1, callback).

  • This also takes time (maybe parsing or calculating), and it also needs a callback.

  • When it finishes, it passes result2.

Same for displayData(result2, callback) — you give it a callback again.

Finally, when everything is done, it logs "Done!".

This approach is bad because you keep nesting functions within other functions creating a triangle like shaped called Pyramid of doom. It furthermore becomes hard to read, debug and handle errors.

This is where promises becomes handy because the promised version of this code would really be simple to read and understand.

getData()
  .then(processData)
  .then(displayData)
  .then(() => console.log("Done!"))
  .catch(err => console.error(err));

The promised version of the code is easy to read and understand:

Step one, getData() returns a promise that will resolve with result1.

Step two, .then(processData) basically says, "When getData() finishes, pass its result (result1) to processData." processData(result1) also returns a Promise, which resolves with result2.

Step three, .then(displayData) Same logic: When processData is done, pass its result to displayData which also returns a Promise. .then(() => console.log("Done!")) runs when all the steps before are complete to log "Done!".

The final step, .catch(err => console.error(err)) catches and logs errors if anything fails.

3.2. Anatomy of a promise

A promise is made up of three parts

  • Pending: Which means not yet completed

  • Resolved(Fulfilled): Which means completely successful and

  • Rejected: Which means failed with an error.

A real life example of promises could be when ordering a plane ticket. You go to the counter and buy a ticket (request), the airline then promises your seat (future result), and the seat will either be confirmed (resolved) and cancelled (rejected). But you always have to wait to get a result later.

Here is a snippet of a promise code:

let promise = new Promise((resolve, reject) => {
  // Some async task (like fetching data)
  let success = true;

  if (success) {
    resolve("It worked!");
  } else {
    reject("Something went wrong.");
  }
});

You cannot see the results immediately, and therefore, you have to use .then() and .catch() to handle it later as shown below.

promise
  .then(result => {
    console.log("Success:", result);
  })
  .catch(error => {
    console.error("Failed:", error);
  });

Let’s simulate a task like loading data that takes 2 seconds:

function loadData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const success = true;

            if (success) {
                console.log("Data loaded");
            } else {
                console.log("Failed to load data");
            }
        }, 2000);
    });
}

// Calling loadData
loadData()
    .then(data => {
        console.log("Good!", data);
    })
    .catch(error => {
        console.log("Bad!", error);
    });

// Output
...waits 2 seconds...
Good! Data loaded

The .then() enables you to chain promises as shown below.

loadData()
  .then(data => {
    console.log("Step 1:", data);
    return "Next step data";
  })
  .then(next => {
    console.log("Step 2:", next);
  })
  .catch(err => {
    console.error("Error:", err);
  });
  1. Async / Await

In the past, to handle asynchronous operations, you used the callback function. However, nesting many callback functions can make your code more difficult to maintain, resulting in a notorious issue known as callback hell.

To avoid this callback hell issue, ES6 introduced the promises that allow you to write asynchronous code in more manageable ways. However, promises often need to be tied together in verbose, arbitrary-looking ways, or at least be chained in .then() and .catch() all the time making the code somewhat a bit complicated but not too complicated like with the callbacks.

To avoid this issue as well, ES2017 introduced the async/await keywords. The async/await syntax helps you work with promise objects, making your code cleaner and clearer.

The async keyword makes the function return a promise while the await keyword waits for a promise to resolve before moving on (It pauses the function execution until the Promise is ready).

NOTE: You can only use await inside an async function. If you try using await without async then it will always throw an error.

Here is a simple example using async / await.

function boilWater() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("Water is boiled!");
    }, 3000);
  });
}

async function cook() {
  console.log("Start boiling water...");
  const result = await boilWater(); // wait 3 seconds
  console.log(result);
  console.log("Now add vegetables.");
}

cook();


// Start boiling water...
// (wait 3 seconds...)
// Water is boiled!
// Now add vegetables.

The const result = await boilWater(); waits for boilWater() to finish (3 seconds) and while it’s waiting, JavaScript can do other tasks in the background. The console.log(result); runs after the Promise resolves with "Water is boiled!".

Furthermore, with async / await, we can use try / catch blocks for error handling as shown below.

function getIngredients() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("Couldn't find ingredients!");
    }, 2000);
  });
}

async function makeDish() {
  try {
    const ingredients = await getIngredients();
    console.log("Got ingredients:", ingredients);
  } catch (error) {
    console.log("Error:", error);
  }
}

makeDish();

// Error: Couldn't find ingredients!

Now somebody may be asking. Why don’t we just make boilingWater() an async function and call it with await directly instead of creating two functions? Why not simplify into one function like this?

async function boilWater() {
  const result = await new Promise(resolve => {
    setTimeout(() => {
      resolve("Water is boiled!");
    }, 3000);
  });

  console.log(result);
}

boilWater();

The answer is: This is absolutely doable and you don’t need too many functions. However, boilWater() in this case is assumed to be a reusable utility that waits and returns a result, while cook() is a process or recipe that uses multiple steps: boilWater(), maybe chopVeges(), serveFood(), etc.

A real world example of async / await is in fetching data from an API as shown below:

async function getUser() {
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
  const user = await response.json(); // Convert to JSON
  console.log(user);
}

getUser();
  1. Fetching data from APIs

API stands for Application Programming Interface. APIs allow different software applications to communicate with each other, enabling developers to access and retrieve data from various sources.

Before delving into fetching data from APIs, let’s first understand HTTP protocol.

HTTP stands for Hyper Text Transfer Protocol. It is the language that the browser and web server uses to communicate to each other. It also defines a set of rules for exchanging data like text, images, videos, and other files across the web.

HTTP follows a client server model, where the client (browser) sends a request to a server (e.g., an HTML page), the server processes the request and sends back a response.

Fetching data from an API in JavaScript is asynchronous, with the fetch API which is a modern approach being used to retrieve data from a server asynchronously. fetch API also offers a promise based interface, which’s more intuitive and easier to manage compared to traditional callback methods.

Request Methods

Request methods basically tell the server what action you want to perform on a resource. Some of the most common ones include:

  • GET: GET requests are used to retrieve resource representation/information only and doesn’t modify it in any way. Since it doesn’t change the resource’s state, it’s also referred to as a safe method.

  • POST: POST is used to create new subordinate resources. It creates new resource into the collection of resources.

  • PUT: It is primarily used to update an existing resource. If the resource doesn’t exist, then the API may decide to create a new resource or not.

  • DELETE: As the name suggests, it is used to delete resources. If you delete a resource, it is removed from the collection of resources.

  • PATCH: PATCH is used to make partial updates on a resource. It is the perfect method for partially updating an existing resource, and therefore, PUT should be used if replacing a resource in its entirety.

Status Codes

Status codes are numbers in the response that tells us the result of the request. They are categorized as follows:

  • 200s (Success): Indicates that the request has succeeded.

  • 300s (Redirection): Means the resource might be moved and therefore a new location is provided.

  • 400s (Client Errors): e.g., 404. Could mean that there was a mistake in the request.

  • 500 (Server Error): Something might be wrong on the server’s side.

Headers

Headers provide additional information about the server’s response to an HTTP request. These headers are essential for developers and server admins to ensure efficient and secure communication between clients and servers.

The response header contains the following parts:

  • Content-Type: Specifies the MIME type which tells the browser how to interpret the content. e.g., “text/html“ tells the browser to display content as HTML document, “application/json” for JSON data, and “image/png“ fro images.

  • Authorization: Provides credentials to access protected resources.

  • Cache-Control: Gives instructions on how to cache content. Value can include directive such as “public“ to allow caching by any client or “private” to limit caching to the browser or other clients.

  • Location: Redirects client to a different URL.

  • Set-Cookies: Used to set cookies in the client’s browser. Cookies are small pieces of data that can be used to store information about the user or their session. etc.

JavaScript Fetch API

Fetch API is a modern and powerful tool that simplifies making HTTP requests directly from web browsers. It leverages promise providing a cleaner, more flexible way to interact with servers. Basically, it handles asynchronous requests and responses more intuitively.

The fetch() takes one mandatory argument – the URL of the resource you want to fetch. Optionally, you can include an object as the second argument, where you can specify various settings such as the HTTP method, headers, and more.

The fetch() returns a promise and therefore .then() and .catch() methods can be used to handle it. Once the request is completed, the resource becomes available, and the promise resolves into a Response object.

For example: Fetching TODO Item from a Free API

fetch("https://jsonplaceholder.typicode.com/todos/1")
  .then(response => response.json())   // convert response to JSON
  .then(data => {
    console.log("Todo:", data);        // log the data
  })
  .catch(error => {
    console.log("Error:", error);      // handle errors
  });

/*
    The output
    {
      userId: 1,
      id: 1,
      title: "delectus aut autem",
      completed: false
    }
*/

An even more cleaner and modern way of handling the fetch is using async / await.

async function fetchTodo() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
    const data = await response.json();
    console.log("Todo:", data);
  } catch (error) {
    console.log("Error fetching data:", error);
  }
}

fetchTodo();

In the above example, we are basically fetching data from the API endpoint. It is not too complicated. However, let us also look at an example of sending data to a server.

async function createPost() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        title: "My New Post",
        body: "This is the content",
        userId: 1
      })
    });

    const result = await response.json();
    console.log("Created Post:", result);
  } catch (error) {
    console.log("Error creating post:", error);
  }
}

createPost();

Here we are basically communicating by telling the server; “Hey server, I want to create a new post. Here’s my data, the title, the content, and which user it’s from. I’ll wait for your response. If something goes wrong, tell me“

{
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    title: "My New Post",
    body: "This is the content",
    userId: 1
  })
}

The above snippet of code has the following important information:

  • method: “POST”: Telling the API that you want to create a new resource (not just get data).

  • headers: {“Content-Type”: “application/json“}: This tells the server, “Hey, I’m sending data in JSON format“

  • body: JSON.stringify{…}: We are sending the actual data (title, body, userId) to the server.

  • JSON.stringify(): Turns your JSON object into a JSON string.

0
Subscribe to my newsletter

Read articles from Lord Abiolla directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Lord Abiolla
Lord Abiolla

Passionate Software Developer with a strong enthusiasm for data, technology, and entrepreneurship to solve real-world problems. I enjoy building innovative digital solutions and currently exploring new advancements in data, and leveraging my skills to create impactful software solutions. Beyond coding, I have a keen interest in strategic thinking in business and meeting new people to exchange ideas and collaborate on exciting projects.