Demystifying JavaScript: Understanding the Event Loop, Asynchronous Programming,Promises & More…
First and foremost, let’s get one thing straight: Java and JavaScript are not the same. It’s a bit like confusing a car and a carpet — both serve entirely different purposes. While they share a part of their name, their worlds are as distinct as night and cheese. Java is a robust, object-oriented programming language favored for building complex, server-side applications. In contrast, JavaScript is the quirky cousin of web development, empowering browsers to add interactivity and spunk to your favorite websites. So, remember, when it comes to Java and JavaScript, it’s not a matter of ‘two peas in a pod’ but more like ‘apples and oranges’ in the coding orchard.
Contents :
- JavaScript: Exploring Its Significance and Why It’s Worth Learning
- Intro to JS core .
- Event Loop
- Callback Function
- Async/Await.
- Axios.
1. The Essence of JavaScript
JavaScript, often abbreviated as JS, is a high-level, dynamic, and interpreted programming language primarily employed for client-side web development. Its paramount function is to introduce behavior to web pages. Unlike HTML (Hypertext Markup Language) and CSS (Cascading Style Sheets), which focus on structuring and styling web content, JavaScript breathes life into these pages, endowing them with the ability to respond to user actions and execute complex functionalities.
JavaScript is a foundational component of modern web development for several compelling reasons:
- Client-Side Interactivity: JavaScript empowers web developers to create dynamic and interactive web pages. It enables features like form validation, animations, real-time updates, and responsive user interfaces, enhancing the overall user experience.
- Cross-Browser Compatibility: JavaScript is supported by all major web browsers, making it a versatile and reliable choice for building web applications that work consistently across various platforms.
- Open Standard: JavaScript is an open and standardized language maintained by the ECMAScript organization. This standardization ensures that JavaScript code written today will remain relevant and functional in the future.
- Versatility: JavaScript isn’t confined to web browsers. It can also be used for server-side development (Node.js), mobile app development (React Native), desktop app development (Electron), and more. This versatility makes it a valuable skill for developers.
- Rich Ecosystem: JavaScript boasts a vast ecosystem of libraries and frameworks (e.g., React, Angular, and Vue.js) that streamline development and offer pre-built solutions for common web development challenges.
- Large Community: JavaScript has a massive and active community of developers. This means that you can easily find resources, tutorials, and help when you encounter issues during development.
- Serverless and Microservices: With technologies like Node.js, JavaScript can be used on the server-side, making it easier to create serverless and microservices-based applications, which are scalable and cost-effective.
- Rapid Development: JavaScript allows for quick development cycles, making it an excellent choice for building prototypes and Minimum Viable Products (MVPs).
2 . JS Core Concepts :
In the early days of the internet, websites were fairly static and relied on simple HTML pages. But as web applications evolved, so did the need for intensive operations like fetching data from external APIs. To address these demands in JavaScript, developers turned to asynchronous programming techniques.
JavaScript is a single-threaded language with a synchronous execution model, which means it processes one operation at a time. However, tasks like making API requests can be time-consuming, potentially leading to unresponsive user experiences. To prevent this blocking behavior, the browser environment provides asynchronous Web APIs that allow certain tasks to run in parallel while keeping the user interface responsive.
Now if you have a doubt regarding what is an API, then ….
The Event Loop:
To understand how JavaScript handles asynchronous code, we must grasp the concept of the event loop. We’ll start with a demonstration of the event loop in action and then break it down into two key components: the stack and the queue.
Without asynchronous Web APIs, JavaScript executes code synchronously, one statement at a time. For example, consider the following code:
function first() {
console.log(1);
}
function second() {
console.log(2);
}
function third() {
console.log(3);
}
first();
second();
third();
The output here will follow the order of function calls: 1, 2, 3.
However, when asynchronous tasks come into play, things get more intricate. For instance, the setTimeout
function, which simulates an asynchronous action with a delay:
function first() {
console.log(1);
}
function second() {
setTimeout(() => {
console.log(2);
}, 0);
}
function third() {
console.log(3);
}
first();
second();
third();
Surprisingly, even with the setTimeout
set to 0 milliseconds, the output will be 1, 3, 2. This happens because JavaScript relies on the event loop, which uses a stack and a queue to manage code execution.
Stack:
The stack, or call stack, keeps track of the functions that are currently running. It follows the “Last in, First out” (LIFO) principle. For the synchronous code example, the browser executes functions in this order:
- Add
first()
to the stack, execute it (logs 1), and remove it from the stack. - Add
second()
to the stack, execute it (logs 2), and remove it from the stack. - Add
third()
to the stack, execute it (logs 3), and remove it from the stack.
In the asynchronous example with setTimeout
:
- Add
first()
to the stack, execute it (logs 1), and remove it from the stack. - Add
second()
to the stack. - Add
setTimeout()
to the stack and start a timer for the anonymous function, adding it to the queue. RemovesetTimeout()
from the stack. - Remove
second()
from the stack. - Add
third()
to the stack, execute it (logs 3), and remove it from the stack.
Later, the event loop detects the anonymous function in the queue and pushes it onto the stack, resulting in the delayed log of 2.
Queue:
The queue, also known as the message queue or task queue, is where functions wait for execution. When the call stack is empty, the event loop checks the queue for pending messages, executing them in the order they arrived.
In the setTimeout
example, the anonymous function runs after the synchronous code execution because it's placed in the queue.
In addition to the main queue, there’s also a job queue or microtask queue that handles promises. Microtasks, like promises, have a higher priority than macrotasks (e.g., setTimeout
).
Now that we understand how the event loop uses the stack and queue, let’s explore the original approach to handling asynchronous code: callback functions.
Callback Functions:
In situations where you need to control the execution order of asynchronous code, you can use callback functions. Callbacks are functions passed as arguments to other functions, often referred to as higher-order functions.
Here’s a basic example of a callback in action:
function regularFunction() {
console.log('Just a function');
}
function higherOrderFunction(callback) {
callback();
}
higherOrderFunction(regularFunction);
Running this code produces the output: “Just a function.”
Now, let’s revisit the first()
, second()
, and third()
functions and modify them to incorporate a callback:
function first() {
console.log(1);
}
function second(callback) {
setTimeout(() => {
console.log(2);
callback();
}, 0);
}
function third() {
console.log(3);
}
To ensure that third()
runs after the asynchronous action in second()
, we pass third
as a callback to second
:
first();
second(third);
This results in the output: 1, 2, 3. Callback functions allow you to defer execution until an asynchronous task is complete, without blocking the main thread.
It’s important to note that callback functions themselves aren’t asynchronous; they are a way to handle asynchronous operations and their outcomes. However, using callbacks extensively can lead to deeply nested and hard-to-read code, often referred to as the “pyramid of doom.”
The Pyramid of Doom:
While callback functions are effective for handling asynchronous execution, they can lead to complex and nested structures known as the “pyramid of doom” or “callback hell.” This occurs when you have multiple consecutive asynchronous operations that depend on one another.
Here’s an example illustrating the pyramid of doom:
function pyramidOfDoom() {
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(3);
}, 500);
}, 2000);
}, 1000);
}
When this code is executed, it results in the output: 1, 2, 3.
In real-world scenarios, managing error handling and data passing between nested callbacks can make the code even more challenging to read and maintain.
Consider the following code as a more complex example:
function asynchronousRequest(args, callback) {
if (!args) {
return callback(new Error('Something went wrong.'));
} else {
return setTimeout(() => callback(null, { body: args + ' ' + Math.floor(Math.random() * 10) }), 500);
}
}
function callbackHell() {
asynchronousRequest('First', function first(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest('Second', function second(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest(null, function third(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
})
})
})
}
// Execute
callbackHell()
In this code, you must make every function account for a possible response
and a possible error
, making the function callbackHell
visually confusing.
Running this code will give you the following:
Output
First 9
Second 3
Error: Whoa! Something went wrong.
at asynchronousRequest (<anonymous>:4:21)
at second (<anonymous>:29:7)
at <anonymous>:9:13
So,the concept of Promises came :
Promises:
A promise represents the eventual completion of an asynchronous operation. Unlike traditional callback functions, promises come with additional features and a cleaner syntax. As developers, we often consume promises provided by asynchronous Web APIs rather than creating them. Let’s explore how to create and consume promises.
Creating a Promise:
To create a promise, use the new Promise syntax, initializing it with a function that takes two parameters: resolve and reject. These functions handle the success and failure of the operation.
const promise = new Promise((resolve, reject) => {
// code
})
Initially, a promise is in the “pending” state with an undefined value. You can fulfill the promise by resolving it:
const promise = new Promise((resolve, reject) => {
resolve('resolved')
})
After resolution, the promise transitions to the “fulfilled” state with the resolved value.
Consuming a Promise:
Promises have a method called “then” which allows you to access the promise’s value once it’s resolved. For example:
promise.then((response) => {
console.log(response)
})
Promises can also be chained, allowing you to pass data to multiple asynchronous operations in a more readable and synchronous-like fashion:
promise
.then((firstResponse) => {
return firstResponse + ' chaining'
})
.then((secondResponse) => {
console.log(secondResponse)
})
Error Handling:
Promises can handle errors using the “catch” method. If an error occurs during the promise execution, it’s caught by the “catch” block:
getUsers(false)
.then((response) => {
console.log(response)
})
.catch((error) => {
console.error(error)
})
Async/Await:
While promises provide a more structured way to deal with asynchronous code, some developers prefer a more synchronous format. To address this, ES7 introduced “async” functions and the “await” keyword. These functions make working with promises feel more like traditional JavaScript.
Creating an Async Function:
Async functions are defined using the “async” keyword. They return promises, making them easy to work with:
async function getUser() {
return {}
}
Consuming Promises with Async/Await:
You can use the “await” keyword within an async function to wait for a promise to settle:
async function getUser() {
const response = await fetch('https://api.github.com/users/octocat')
const data = await response.json()
console.log(data)
}
Handling Errors with Async/Await:
Async functions can also handle errors using try/catch, making error handling more straightforward:
async function getUser() {
try {
const response = await fetch('https://api.github.com/users/octocat')
const data = await response.json()
console.log(data)
} catch (error) {
console.error(error)
}
}
Streamlining Asynchronous Requests with Axios:
In the world of asynchronous JavaScript, making HTTP requests to external APIs or servers is a common and essential task. While Promises and Async/Await have simplified how we handle asynchronous operations, using Axios takes the process of making these requests to the next level.
What is Axios?
Axios is a popular JavaScript library that serves as an HTTP client. It streamlines the process of sending HTTP requests and handling responses, making it a top choice among developers for its user-friendly interface and extensive feature set.
Before you can start using Axios, you need to install it in your project. You can do this using a package manager like npm or yarn. Here’s an example of how to install Axios using npm:
npm install axios
then import in js file
import axios from ‘axios’;
Making a GET Request:
Let’s dive into an example. Suppose you want to fetch data from an external API, such as the JSONPlaceholder API (a fake online REST API for testing and prototyping). Here’s how you can use Axios to make a GET request and handle the response:
const axios = require('axios');
// Define the API URL
const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1';
// Make a GET request
axios.get(apiUrl)
.then((response) => {
// Handle the successful response
console.log('Data fetched successfully:', response.data);
})
.catch((error) => {
// Handle any errors
console.error('Error fetching data:', error);
});
In this example, we import Axios, define the API URL we want to fetch data from, and then use axios.get()
to make a GET request. We handle the response using Promises with .then()
and error handling with .catch()
.
Making a POST Request:
Axios makes it just as simple to send data to a server using a POST request. Here’s an example:
const axios = require('axios');
// Define the API URL
const apiUrl = 'https://jsonplaceholder.typicode.com/posts';
// Data to be sent in the request
const postData = {
title: 'My New Post',
body: 'This is the content of my new post.',
userId: 1,
};
// Make a POST request
axios.post(apiUrl, postData)
.then((response) => {
// Handle the successful response
console.log('Data sent and received successfully:', response.data);
})
.catch((error) => {
// Handle any errors
console.error('Error sending data:', error);
});
In this POST request example, we define the API URL and the data we want to send. We use axios.post()
to make the request and handle the response or errors as before.
Axios, with its promise-based approach, simplifies the process of making asynchronous HTTP requests. Whether you’re fetching data or sending it to a server, Axios enhances your JavaScript development experience by offering a clean and organized way to handle HTTP interactions. Combining Axios with Promises or Async/Await allows you to manage asynchronous operations efficiently, making it a must-have tool for any JavaScript developer.
In conclusion, JavaScript, with its versatility and ubiquity, stands as a cornerstone of modern web development. Embracing its intricacies empowers developers to create immersive and dynamic digital experiences that define the internet landscape. I hope this article has enriched your understanding of JavaScript and inspired you to dive deeper into the world of web development. So, keep coding with the magic of JavaScript !