whenever life put's you in a tough situtation, never say why me! but, try me!

Chapter 11: Asynchronous Programming

Asynchronous programming is essential for handling tasks that take time to complete, such as fetching data from a server or reading files. This chapter covers callbacks, promises, async/await, and the Fetch API for managing asynchronous operations in JavaScript.

Callbacks

What is a Callback?

A callback is a function that is passed as an argument to another function and is executed after the completion of that function.

Example of a Callback

function fetchData(callback) {
  setTimeout(() => {
    const data = "Data loaded";
    callback(data);
  }, 2000);
}

fetchData(function (result) {
  console.log(result); // Output: Data loaded
});

Handling Errors with Callbacks

In asynchronous functions that use callbacks, errors are typically passed as the first argument to the callback.

Example:

function fetchData(callback) {
  setTimeout(() => {
    const error = false;
    if (error) {
      callback("Error occurred", null);
    } else {
      callback(null, "Data loaded");
    }
  }, 2000);
}

fetchData((error, result) => {
  if (error) {
    console.error(error);
  } else {
    console.log(result);
  }
});

In this example, fetchData simulates an asynchronous operation using setTimeout, and once the operation is complete, it calls the provided callback function with the result.

Promises in JavaScript

Promises are a foundational concept in JavaScript for managing asynchronous operations. They represent a value that may be available now, in the future, or never. Promises provide a way to handle asynchronous results and errors more cleanly compared to traditional callback functions.

What is a Promise?

A promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value. A promise has three states:

  • Pending: The initial state, neither fulfilled nor rejected.
  • Fulfilled: The operation completed successfully.
  • Rejected: The operation failed.

Creating a Promise

A promise is created using the Promise constructor, which takes a single function as an argument. This function is called the executor and receives two functions: resolve and reject.

Example:

let myPromise = new Promise((resolve, reject) => {
  let success = true;
  if (success) {
    resolve("Operation was successful");
  } else {
    reject("Operation failed");
  }
});

Promises Methods

then()

The then() method is used to specify what should be done when a promise is fulfilled. It takes two arguments: a callback function for the fulfilled case and an optional callback function for the rejected case.

Syntax:

promise.then(onFulfilled, onRejected);

Example:

myPromise
  .then((result) => {
    console.log(result); // Output: Operation was successful
  })
  .catch((error) => {
    console.error(error); // This won't be executed
  });

catch()

The catch() method is used to specify what should be done when a promise is rejected. It takes one argument: a callback function that handles the error.

Syntax:

promise.catch(onRejected);

Example:

myPromise.catch((error) => {
  console.error(error); // Output: Operation failed
});

finally()

The finally() method is used to execute code after a promise is settled (fulfilled or rejected), regardless of its outcome. It takes one argument: a callback function that will be executed after the promise settles.

Syntax:

promise.finally(onFinally);

Example:

myPromise.finally(() => {
  console.log("Promise has been settled"); // This will be executed no matter the outcome
});

Chaining Promises

Promises can be chained to perform a series of asynchronous operations sequentially. Each then() returns a new promise, which allows you to chain additional operations.

Example:

myPromise
  .then((result) => {
    console.log(result);
    return "Another operation";
  })
  .then((newResult) => {
    console.log(newResult); // Output: Another operation
  })
  .catch((error) => {
    console.error(error); // Handles any error from previous steps
  })
  .finally(() => {
    console.log("All done");
  });

Promise.all()

The Promise.all() method takes an array of promises and returns a single promise that resolves when all of the promises in the array have resolved, or rejects if any of the promises reject.

Syntax:

Promise.all(iterable);

Example:

let promise1 = Promise.resolve("First");
let promise2 = Promise.resolve("Second");
let promise3 = Promise.resolve("Third");

Promise.all([promise1, promise2, promise3])
  .then((results) => {
    console.log(results); // Output: ['First', 'Second', 'Third']
  })
  .catch((error) => {
    console.error(error);
  });

Promise.race()

The Promise.race() method takes an array of promises and returns a single promise that resolves or rejects as soon as one of the promises in the array resolves or rejects, with the value or reason from that promise.

Syntax:

Promise.race(iterable);

Example:

let promise1 = new Promise((resolve, reject) =>
  setTimeout(resolve, 500, "One")
);
let promise2 = new Promise((resolve, reject) =>
  setTimeout(resolve, 100, "Two")
);

Promise.race([promise1, promise2])
  .then((result) => {
    console.log(result); // Output: 'Two'
  })
  .catch((error) => {
    console.error(error);
  });

Promise.allSettled()

The Promise.allSettled() method takes an array of promises and returns a single promise that resolves after all of the given promises have either resolved or rejected, with an array of objects that each describe the outcome of each promise.

Syntax:

Promise.allSettled(iterable);

Example:

let promise1 = Promise.resolve("Success");
let promise2 = Promise.reject("Failure");

Promise.allSettled([promise1, promise2]).then((results) => {
  results.forEach((result, index) => {
    if (result.status === "fulfilled") {
      console.log(`Promise ${index} resolved with: ${result.value}`);
    } else {
      console.error(`Promise ${index} rejected with: ${result.reason}`);
    }
  });
});

Promise.any()

The Promise.any() method takes an array of Promise objects and, as soon as one of the promises in the array fulfills, returns a single promise that resolves with the value from that promise. If no promises in the array fulfill (i.e., all are rejected), then it returns a promise that rejects with an AggregateError, a special kind of error that groups together individual errors.

Syntax:

Promise.any(iterable);

Example:

let promise1 = Promise.reject("Failure 1");
let promise2 = Promise.reject("Failure 2");
let promise3 = Promise.resolve("Success");

Promise.any([promise1, promise2, promise3])
  .then((result) => {
    console.log(result); // Output: 'Success'
  })
  .catch((error) => {
    console.error(error);
  });

Async/Await in JavaScript

async and await are syntactic sugar built on top of Promises. They make working with asynchronous code more readable and easier to manage compared to using Promises directly.

What is async?

The async keyword is used to declare an asynchronous function. An asynchronous function is a function that implicitly returns a Promise. This allows you to use the await keyword within the function.

Syntax:

async function functionName() {
  // Function body
}

Example:

async function fetchData() {
  return "Data fetched";
}

fetchData().then((result) => console.log(result)); // Output: Data fetched

In the example above, fetchData() is an asynchronous function that returns a Promise. The value 'Data fetched' is wrapped in a resolved Promise automatically.

What is await?

The await keyword is used inside async functions to pause the execution of the function until the Promise it is waiting for resolves. This allows you to write asynchronous code in a synchronous style, making it easier to read and maintain.

Syntax:

let result = await promise;

Example:

async function fetchData() {
  let result = await new Promise((resolve) =>
    setTimeout(() => resolve("Data fetched"), 1000)
  );
  console.log(result); // Output: Data fetched
}

fetchData();

In this example, await pauses the execution of fetchData() until the Promise resolves, then assigns the resolved value to result and logs it to the console.

Error Handling with async/await

Error handling in asynchronous functions is done using try/catch blocks. This approach is more intuitive compared to handling errors with .catch() in Promises.

Example:

async function fetchData() {
  try {
    let result = await new Promise((_, reject) =>
      setTimeout(() => reject("Error occurred"), 1000)
    );
    console.log(result);
  } catch (error) {
    console.error(error); // Output: Error occurred
  }
}

fetchData();

In this example, if the Promise is rejected, the error is caught in the catch block.

Chaining Async/Await

You can chain multiple await expressions in an async function to perform a series of asynchronous operations sequentially.

Example:

async function processData() {
  try {
    let data = await fetchDataFromServer();
    let processedData = await processData(data);
    console.log(processedData);
  } catch (error) {
    console.error(error);
  }
}

async function fetchDataFromServer() {
  return "Data from server";
}

async function processData(data) {
  return `Processed ${data}`;
}

processData();

In this example, fetchDataFromServer() and processData() are called sequentially. If any of the Promises are rejected, the error will be caught by the catch block.

async/await with Promises Methods

async/await can be used with methods that return Promises, such as Promise.all(), Promise.race(), and others.

Example with Promise.all():

async function fetchAllData() {
  try {
    let [data1, data2] = await Promise.all([
      fetchDataFromServer1(),
      fetchDataFromServer2(),
    ]);
    console.log(data1, data2);
  } catch (error) {
    console.error(error);
  }
}

async function fetchDataFromServer1() {
  return "Data from server 1";
}

async function fetchDataFromServer2() {
  return "Data from server 2";
}

fetchAllData();

Example with Promise.race():

async function fetchFastestData() {
  try {
    let result = await Promise.race([
      fetchDataFromServer1(),
      fetchDataFromServer2(),
    ]);
    console.log(result);
  } catch (error) {
    console.error(error);
  }
}

fetchFastestData();

Key Points

  • async Functions: Functions declared with the async keyword. They always return a Promise.
  • await Expressions: Used inside async functions to pause execution until a Promise resolves or rejects.
  • Error Handling: Use try/catch blocks within async functions to handle errors.
  • Chaining: You can use multiple await expressions to handle sequential asynchronous operations.
  • Integration with Promises Methods: async/await works seamlessly with Promise methods such as Promise.all(), Promise.race(), etc.

Fetch API

The Fetch API is a modern way to make HTTP requests in JavaScript. It is designed to replace the older XMLHttpRequest method and provides a more powerful and flexible approach to handling network requests and responses.

Basic Syntax

The fetch() function is used to initiate a network request. It returns a Promise that resolves to the Response object representing the response to the request.

Syntax:

fetch(url, options);
  • url – The URL to which the request is sent.
  • options (optional) – An object containing any custom settings for the request.

Example:

fetch("https://api.example.com/data")
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error("Error:", error));

Fetch Options

The options parameter allows you to customize the request. It can include various properties such as method, headers, body, and more.

Common Options:

  • method: The HTTP method to use (e.g., GET, POST, PUT, DELETE).
  • headers: An object containing request headers.
  • body: The request body, typically used with POST or PUT requests.

Example:

fetch("https://api.example.com/data", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ key: "value" }),
})
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error("Error:", error));

Handling Responses

The fetch() function returns a Promise that resolves to a Response object. The Response object provides methods to extract the data from the response.

Key Methods:

  • response.json(): Parses the response body as JSON.
  • response.text(): Parses the response body as text.
  • response.blob(): Parses the response body as a Blob (binary data).
  • response.formData(): Parses the response body as FormData.

Example:

fetch("https://api.example.com/data")
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
  })
  .catch((error) => console.error("Error:", error));

Error Handling

The Fetch API does not reject the Promise on HTTP error statuses (e.g., 404 or 500). Instead, it resolves the Promise and you need to manually check the response.ok property to determine if the request was successful.

Example:

fetch("https://api.example.com/data")
  .then((response) => {
    if (!response.ok) {
      throw new Error("Network response was not ok");
    }
    return response.json();
  })
  .then((data) => console.log(data))
  .catch((error) => console.error("Error:", error));

Working with FormData

You can use the FormData API to send data in multipart/form-data format, often used for file uploads.

Example:

const formData = new FormData();
formData.append("key", "value");
formData.append("file", fileInput.files[0]);

fetch("https://api.example.com/upload", {
  method: "POST",
  body: formData,
})
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error("Error:", error));

Abort Requests

You can cancel a fetch request using an AbortController. This is useful for stopping requests that are no longer needed.

Example:

const controller = new AbortController();
const signal = controller.signal;

fetch("https://api.example.com/data", { signal })
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => {
    if (error.name === "AbortError") {
      console.log("Request was aborted");
    } else {
      console.error("Error:", error);
    }
  });

// Abort the request
controller.abort();

JSONP

The Fetch API does not support JSONP (JSON with Padding) directly. If you need to request JSONP data, you must use a different method, such as adding a <script> tag dynamically.