Async JavaScript

Download as pdf or txt
Download as pdf or txt
You are on page 1of 18

Created by Aditya Kshirsagar : Imprenable Automation Training Module

Async JavaScript

What is Asynchronous JavaScript ?


Asynchronous JavaScript refers to the execution of JavaScript code that allows non-
blocking operations. In traditional synchronous programming, each operation is executed
in a sequential manner, meaning that the program waits for each operation to complete
before moving on to the next one. This can be problematic when dealing with time-
consuming tasks, such as making network requests or reading large files, as it can cause
the program to become unresponsive.

Asynchronous JavaScript, on the other hand, enables concurrent operations and utilizes
callbacks, promises, or async/await syntax to handle the completion of those operations.
When an asynchronous operation is initiated, the program continues to execute without
waiting for it to finish. Once the operation is completed, a callback function is invoked, or a
promise is resolved, or an async/await function moves to the next line of code.

Callbacks were the traditional way of handling asynchronous operations in JavaScript. A


callback function is passed as an argument to an asynchronous function, and it is called
once the operation is complete. However, callbacks can lead to callback hell, where
nested callbacks become hard to manage.

Promises were introduced in ECMAScript 6 (ES6) as a more structured way of dealing


with asynchronous operations. A promise represents the eventual completion or failure of
an asynchronous operation and provides methods to handle the results. Promises allow
you to chain multiple asynchronous operations and handle errors more gracefully.

Async/await, introduced in ECMAScript 2017 (ES8), provides a more synchronous-like


way of writing asynchronous code. It allows you to write asynchronous operations using a
more sequential syntax, making the code easier to read and understand. The async
keyword is used to define an asynchronous function, and the await keyword is used to
pause the execution of the function until a promise is resolved.
With asynchronous JavaScript, you can perform tasks such as making AJAX requests,
interacting with databases, or performing time-consuming computations without blocking
the main execution thread, thus improving the responsiveness and performance of your
applications.

Async Code in Action :


An example that demonstrates how asynchronous JavaScript code works using the
async/await syntax :

// Asynchronous function that returns a promise


function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data successfully fetched!');
}, 2000);
});
}

// Async function that uses the await keyword


async function process() {
console.log('Start');

try {
const data = await fetchData(); // Pause here until the promise is resolved
console.log(data);
console.log('End');
} catch (error) {
console.log('Error:', error);
}
}

process();

In this example, we have an asynchronous function fetchData that simulates fetching


data from a remote server. It returns a promise that resolves after a delay of 2 seconds.

The process function is defined as an async function. Inside this function, we first log
'Start' to the console. Then, we use the await keyword to pause the execution of the
function until the promise returned by fetchData is resolved. Once the promise is
resolved, the value of the resolved promise is assigned to the data variable, and we log it
to the console along with 'End'.

If an error occurs during the promise resolution, the code inside the catch block will be
executed, and the error will be logged to the console.

When we call process() , it initiates the execution of the async function. The program will
log 'Start', then pause at the await fetchData() line until the promise resolves. After 2
seconds, the promise resolves, and the program logs the fetched data and 'End' to the
console.

This is a simplified example, but it illustrates how asynchronous JavaScript code using
async/await allows you to write code that appears to be synchronous while performing
asynchronous operations in the background.

What are HTTP requests in JS ?


In JavaScript, HTTP requests are used to send and receive data from a server using the
HTTP protocol. This allows your JavaScript code to interact with web servers, retrieve
data, and update data on the server.

There are several ways to make HTTP requests in JavaScript, including :

1. XMLHttpRequest : This is an older and lower-level API for making HTTP requests. It
provides functionality to create and send requests, handle responses, and manage
the state of the request. However, it can be more complex to use compared to
modern alternatives.
2. Fetch API : The Fetch API is a newer and more modern way to make HTTP requests
in JavaScript. It provides a simpler and more streamlined interface for sending and
receiving data. It uses promises to handle the response and provides a more flexible
and powerful feature set compared to XMLHttpRequest.

Here's an example that demonstrates how to make an HTTP GET request using the Fetch
API :

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

In this example, we use the fetch function to send a GET request to the specified URL
( https://api.example.com/data ). The fetch function returns a promise that resolves to
the Response object representing the response.

We can then use the .json() method of the Response object to parse the response body
as JSON. The method also returns a promise, so we can chain another .then() to
handle the parsed data.

If an error occurs during the request or response handling, the catch block will be
executed, allowing you to handle and log any errors.

3. Third-party libraries : There are also popular third-party libraries like Axios and
jQuery's AJAX functionality that provide additional features and abstractions for
making HTTP requests. These libraries often have simplified and more consistent
APIs, making it easier to work with HTTP requests in JavaScript.

Overall, HTTP requests in JavaScript allow you to communicate with servers, retrieve
data, and perform various operations on the web. The choice of which method to use
depends on your specific requirements and the level of simplicity or sophistication you
need in your code.

Making HTTP Requests (XHR) :


To make HTTP requests using XMLHttpRequest (XHR), you can follow these steps :

1. Create an instance of the XMLHttpRequest object :

var xhr = new XMLHttpRequest();

2. Set up the request by specifying the method (e.g., GET, POST), URL, and optional
asynchronous flag :

xhr.open('GET', 'https://api.example.com/data', true); // true for


asynchronous, false for synchronous
3. Set any request headers, such as content type or authorization :

xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer your_token');

4. Register event handlers to handle different states of the request :

xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // Request is complete
if (xhr.status === 200) { // Successful response
var responseData = JSON.parse(xhr.responseText);
console.log('Data:', responseData);
} else {
console.log('Error:', xhr.status);
}
}
};

5. Send the request :

xhr.send();

In this example, we are making a GET request to the URL


https://api.example.com/data . When the request state changes, we check if the request
is complete ( readyState === 4 ) and if the response status is 200, indicating a successful
response. If successful, we parse the response using JSON.parse() and log the data.
Otherwise, we log the error status.

It's important to note that XMLHttpRequest (XHR) is a lower-level API, and the Fetch API
is recommended for modern JavaScript applications due to its simplicity and broader
feature set. However, understanding XHR is still valuable for legacy code or specific use
cases.

Remember to handle errors, sanitize user input, and follow best practices when working
with HTTP requests to ensure the security and reliability of your applications.

Response Status :
In JavaScript, when making HTTP requests using the Fetch API or XMLHttpRequest, the
response status provides information about the outcome of the request. The response
status is represented by a numeric code that indicates the HTTP status of the response.
Here are some commonly encountered status codes and their meanings :

200: OK - The request was successful.


201: Created - The request resulted in a new resource being created.
204: No Content - The server successfully processed the request but does not return
any content.
400: Bad Request - The server could not understand the request due to malformed
syntax or invalid parameters.
401: Unauthorized - The request requires authentication, and the user is not
authenticated.
403: Forbidden - The server understood the request but refuses to authorize it.
404: Not Found - The requested resource could not be found on the server.
500: Internal Server Error - The server encountered an unexpected condition that
prevented it from fulfilling the request.

In both the Fetch API and XMLHttpRequest, you can access the response status through
the status property of the Response object (in Fetch API) or the status property of the
XMLHttpRequest object.

Here's an example using the Fetch API to check the response status :

fetch('https://api.example.com/data')
.then(response => {
if (response.ok) {
console.log('Request succeeded. Status:', response.status);
} else {
console.log('Request failed. Status:', response.status);
}
})
.catch(error => {
console.log('Error:', error);
});

In this example, the response object is received as a parameter in the then() callback.
We can check if the response.ok property is true to determine if the request was
successful. If it is, we can log the status using response.status . If the request fails, we
can also log the status.
It's important to handle different response statuses appropriately in your code to handle
success cases, error cases, and provide appropriate feedback to the user.

Callback Functions :
In JavaScript, a callback function is a function that is passed as an argument to another
function and is invoked or executed at a later point in time. Callback functions are a
fundamental concept in JavaScript, particularly when working with asynchronous
operations or event-driven programming.

Here's an example to illustrate the concept of callback functions :

function greet(name, callback) {


console.log('Hello, ' + name + '!');
callback();
}

function sayGoodbye() {
console.log('Goodbye!');
}

greet('John', sayGoodbye);

In this example, we have two functions: greet and sayGoodbye . The greet function takes
two parameters: name and callback . It logs a greeting message along with the provided
name, and then it invokes the callback function.

The sayGoodbye function is defined separately and logs a farewell message.

When we call greet('John', sayGoodbye) , we pass the sayGoodbye function as the


callback argument. The greet function will execute the greeting message and then
invoke the callback function, which results in the farewell message being logged to the
console.

Callback functions are commonly used in asynchronous operations, such as making


HTTP requests or reading files, where the execution of code may not be immediate, but
instead happens at a later time when the operation is complete. In such cases, the
callback function is often used to handle the result of the asynchronous operation or to
perform additional actions.
It's important to note that callback functions can be synchronous or asynchronous,
depending on the context in which they are used. They can be defined as anonymous
functions or as named functions, and they provide a way to achieve flexibility and control
flow in JavaScript programming.

JSON Data :
JSON (JavaScript Object Notation) is a lightweight data interchange format that is widely
used for storing and exchanging data. It is based on a subset of the JavaScript
programming language syntax and is easy for humans to read and write.

JSON data is represented in key-value pairs and supports several data types, including
strings, numbers, booleans, arrays, and objects. Here's an example of JSON data :

{
"name": "John Doe",
"age": 30,
"isStudent": false,
"hobbies": ["reading", "playing guitar", "hiking"],
"address": {
"street": "123 Main St",
"city": "New York",
"country": "USA"
}
}

In this example, we have a JSON object representing a person's information. It has


properties like "name", "age", and "isStudent" with corresponding values. The "hobbies"
property contains an array of strings, and the "address" property is an embedded object
with nested properties.

In JavaScript, you can work with JSON data using built-in methods and functions :

1. Parsing JSON : To convert a JSON string into a JavaScript object, you can use the
JSON.parse() method :

var jsonString = '{"name":"John Doe","age":30}';


var data = JSON.parse(jsonString);
console.log(data.name); // Output: John Doe
2. Stringifying JavaScript Object : To convert a JavaScript object into a JSON string,
you can use the JSON.stringify() method :

var data = {
name: "John Doe",
age: 30
};
var jsonString = JSON.stringify(data);
console.log(jsonString); // Output: {"name":"John Doe","age":30}

JSON data is commonly used for exchanging data between a client and server in web
applications, storing configuration settings, or transmitting data between different systems
or APIs. It provides a standardized and widely supported format for data representation.

Callback Hell :
Callback hell, also known as the pyramid of doom, is a term used to describe a situation in
asynchronous JavaScript code when multiple nested callbacks are used, leading to code
that is difficult to read, understand, and maintain. It occurs when callbacks are chained
inside callbacks, resulting in deeply nested code structures.

Here's an example that demonstrates callback hell :

asyncFunc1(function(err, result1) {
if (err) {
console.error('Error:', err);
} else {
asyncFunc2(result1, function(err, result2) {
if (err) {
console.error('Error:', err);
} else {
asyncFunc3(result2, function(err, result3) {
if (err) {
console.error('Error:', err);
} else {
// More nested callbacks...
}
});
}
});
}
});

In this example, we have three asynchronous functions: asyncFunc1, asyncFunc2, and


asyncFunc3. Each function takes a callback as a parameter. When one function
completes, it invokes the next function as a callback. As the code grows with more
asynchronous operations, the nesting becomes deeper and harder to manage.

Callback hell can make code difficult to read, understand, and debug. It can also lead to
issues like callback errors being missed or the accidental creation of variables with the
same name due to multiple levels of scopes.

To mitigate callback hell, there are a few approaches you can consider:

Use named functions: Instead of anonymous callback functions, define named functions
that can be reused and make the code structure flatter and more readable.

Use control flow libraries: Libraries like async.js, Promises, or async/await syntax
(supported in modern JavaScript) provide cleaner alternatives for managing
asynchronous operations and avoiding excessive nesting.

Refactor and modularize code: Break down complex code into smaller, modular functions
that are easier to understand and maintain. This allows for better separation of concerns
and reduces the depth of nesting.

Consider using async/await: Asynchronous functions and the async/await syntax provide
a more sequential and readable way to write asynchronous code, avoiding the need for
explicit callbacks and reducing nesting.

By applying these techniques, you can improve the readability and maintainability of your
code, making it easier to work with and understand.

Promise Basics :
Promises are a fundamental concept in JavaScript for handling asynchronous operations
and managing the flow of asynchronous code. They provide a way to work with
asynchronous operations in a more organized and readable manner compared to
traditional callback-based approaches. Here are the basics of working with promises:
1. Creating a Promise : To create a promise, you use the Promise constructor, which
takes a callback function as an argument. This callback function, commonly referred
to as the executor function, has two parameters: resolve and reject . Inside the
executor function, you perform your asynchronous operation and call resolve when
the operation is successful, or reject when an error occurs. Here's an example:

const promise = new Promise((resolve, reject) => {


// Asynchronous operation
setTimeout(() => {
const data = 'Promise resolved!';
resolve(data); // Resolve the promise
// or reject('An error occurred'); // Reject the promise with an error
}, 2000);
});

2. Consuming a Promise : Once you have a promise, you can consume it using the
then() and catch() methods. The then() method is used to handle the successful
resolution of the promise, while the catch() method is used to handle any errors that
occurred during the promise's execution. Here's an example:

promise.then((data) => {
console.log('Success:', data);
}).catch((error) => {
console.log('Error:', error);
});

In this example, the then() method takes a callback function that will be executed when
the promise is resolved successfully. The catch() method takes a callback function that
will be executed if the promise is rejected or an error occurs.

3. Chaining Promises : Promises can be chained together using the then() method,
allowing you to perform multiple asynchronous operations sequentially. Each then()
method returns a new promise, enabling you to chain additional then() or catch()
calls. This helps in avoiding callback hell and maintaining a more readable code
structure. Here's an example:

promise.then((data) => {
console.log('Step 1:', data);
return anotherAsyncOperation(); // Returns a new promise
}).then((result) => {
console.log('Step 2:', result);
}).catch((error) => {
console.log('Error:', error);
});

In this example, after the initial promise is resolved, the first then() method is called,
which performs another asynchronous operation and returns a new promise. The second
then() method is then called, and so on.

Promises provide a powerful tool for working with asynchronous code, allowing for more
readable and manageable code structures. They offer an alternative to callback-based
patterns and enable better error handling through the use of catch() for centralized error
handling.

Additionally, promises can be further enhanced with async/await syntax, which provides a
more sequential and synchronous-looking code structure when working with promises.

Chaining Promises :
Chaining promises is a technique in JavaScript that allows you to execute multiple
asynchronous operations in a sequential manner by chaining then() methods. Each then()
method returns a new promise, enabling you to perform subsequent operations or chain
additional then() or catch() calls.

Here's an example that demonstrates chaining promises :

function asyncOperation1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const result = 'Async Operation 1';
console.log(result);
resolve(result);
}, 2000);
});
}

function asyncOperation2(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const result = `${data} + Async Operation 2`;
console.log(result);
resolve(result);
}, 2000);
});
}

function asyncOperation3(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const result = `${data} + Async Operation 3`;
console.log(result);
resolve(result);
}, 2000);
});
}

asyncOperation1()
.then((result1) => asyncOperation2(result1))
.then((result2) => asyncOperation3(result2))
.then((finalResult) => {
console.log('Final Result:', finalResult);
})
.catch((error) => {
console.error('Error:', error);
});

In this example, we have three asynchronous operations: asyncOperation1(),


asyncOperation2(), and asyncOperation3(). Each operation returns a new promise.

By chaining the promises using then() methods, we ensure that the subsequent
operations are executed only after the previous operation has resolved successfully. The
result of each operation is passed to the next then() method as an argument.

Finally, we use a final then() method to handle the result of the last operation and log the
final result. If any error occurs in any of the promises, the catch() method will be triggered
to handle the error.

This chaining of promises allows for a more readable and sequential flow of asynchronous
operations. It avoids deeply nested callback structures and improves the maintainability of
asynchronous code.
The Fetch API :
The Fetch API is a modern JavaScript interface for making HTTP requests and handling
responses. It provides a more powerful and flexible alternative to the traditional
XMLHttpRequest object for fetching resources asynchronously from a server.

Here's an example of using the Fetch API to make an HTTP GET request :

fetch('https://api.example.com/data')
.then(response => {
if (response.ok) {
return response.json(); // Parse response body as JSON
} else {
throw new Error('Request failed with status: ' + response.status);
}
})
.then(data => {
console.log('Received data:', data);
})
.catch(error => {
console.error('Error:', error);
});

In this example, the fetch() function is called with the URL of the resource we want to
retrieve. It returns a promise that resolves to the Response object representing the
response of the request.

We use the first then() method to check if the response was successful (response.ok). If it
is, we parse the response body as JSON using the json() method, which also returns a
promise.

The second then() method is called with the parsed JSON data, and we can perform
further operations on the data.

If any error occurs during the request or response handling, the catch() method will be
triggered to handle the error.

The Fetch API provides various methods and options for making different types of
requests (GET, POST, PUT, DELETE, etc.), setting headers, handling different types of
response data, and more.
Here's an example of making an HTTP POST request with JSON payload using the Fetch
API :

const url = 'https://api.example.com/data';


const data = { name: 'John', age: 30 };

fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(responseData => {
console.log('Received response:', responseData);
})
.catch(error => {
console.error('Error:', error);
});

In this example, we provide additional options in the fetch() call to specify the HTTP
method as 'POST', set the 'Content-Type' header to 'application/json', and pass the data
object as JSON string in the request body using JSON.stringify().

The Fetch API is supported in modern browsers and provides a more intuitive and flexible
way to work with HTTP requests and responses in JavaScript.

Async & Await :


Async/await is a modern syntax introduced in JavaScript that provides a more readable
and synchronous-like way to write asynchronous code. It builds upon the concept of
promises and simplifies the handling of asynchronous operations.

The async keyword is used to define an asynchronous function, which returns a promise
implicitly. Within an async function, you can use the await keyword to pause the execution
of the function until a promise is resolved. This allows you to write asynchronous code
that looks and behaves more like synchronous code.

Here's an example that demonstrates the usage of async/await :


function asyncOperation1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const result = 'Async Operation 1';
console.log(result);
resolve(result);
}, 2000);
});
}

function asyncOperation2(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const result = `${data} + Async Operation 2`;
console.log(result);
resolve(result);
}, 2000);
});
}

async function performOperations() {


try {
const result1 = await asyncOperation1();
const result2 = await asyncOperation2(result1);
console.log('Final Result:', result2);
} catch (error) {
console.error('Error:', error);
}
}

performOperations();

In this example, we have two asynchronous functions, asyncOperation1 and


asyncOperation2, which return promises. We define an async function named
performOperations that will execute these operations sequentially.

Using the await keyword, we pause the execution of the function until the promise
returned by each asynchronous operation is resolved. This allows us to assign the
resolved values to variables (result1 and result2) and use them in subsequent statements
as if they were synchronous.
The try/catch block is used to handle any errors that occur during the execution of the
async function. If an error is thrown within the async function or any of the awaited
promises are rejected, it will be caught and processed in the catch block.

Async/await provides a more linear and readable code structure compared to chaining
promises or using nested callbacks. It helps to avoid callback hell and makes
asynchronous code easier to write and understand.

It's important to note that async/await can only be used within async functions. However,
async functions can be used with other asynchronous mechanisms such as the Fetch API
or other promise-based operations.

Throwing & Catching Errors :


Throwing and catching errors are essential concepts in JavaScript for handling and
managing exceptions or unexpected situations in your code. You can use the throw
statement to manually throw an error, and the try...catch statement to catch and handle
the thrown error.

Here's an example that demonstrates throwing and catching errors :

function divideNumbers(a, b) {
if (b === 0) {
throw new Error('Divide by zero error');
}
return a / b;
}

try {
const result = divideNumbers(10, 0);
console.log('Result:', result);
} catch (error) {
console.error('Error:', error.message);
}

In this example, the divideNumbers() function is defined to divide two numbers ( a and
b ). If b is equal to zero, we intentionally throw an error using the throw statement and
pass an Error object with a custom error message.
The try block is used to enclose the code that might throw an error. In this case, we call
the divideNumbers() function with arguments 10 and 0 , which triggers the error.

If an error occurs within the try block, the execution jumps to the catch block. The
catch block takes an error parameter, which represents the caught error object. We can
then handle the error as needed, such as logging an error message ( error.message ) to
the console.

By catching errors, we prevent them from causing the script execution to halt and allow for
graceful error handling or recovery.

You can also catch different types of errors by using multiple catch blocks, where each
block is designed to handle a specific type of error. This allows for more fine-grained error
handling.

try {
// Code that might throw an error
} catch (error1) {
// Handle specific error type 1
} catch (error2) {
// Handle specific error type 2
} catch (error3) {
// Handle specific error type 3
}

It's important to note that when an error is thrown, the normal flow of execution is
interrupted, and the program jumps to the nearest catch block. If there is no surrounding
try...catch block to catch the error, it will result in an unhandled exception, and the
script execution may terminate.

By properly throwing and catching errors, you can add robust error handling to your code
and handle exceptional scenarios more effectively.

You might also like