Async Javascript - rs-hash/Learning GitHub Wiki
Async JavaScript
Async JavaScript involves dealing with asynchronous operations in JavaScript to avoid blocking the main thread and to ensure smooth execution of code. There are several concepts related to async JavaScript, and here are some of the key ones along with examples:
1. Callbacks:
Callbacks are functions passed as arguments to other functions. They are commonly used for handling asynchronous operations, such as when making AJAX requests.
Example using setTimeout:
function printMessage() {
console.log("Hello, this is a callback!");
}
setTimeout(printMessage, 1000); // Calls printMessage after 1 second
2. Promises:
Promises are objects used to handle asynchronous operations. They represent a value that may not be available yet, but will be resolved (fulfilled) or rejected at some point in the future.
Example using a Promise with setTimeout:
const delay = (milliseconds) => {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
};
delay(1000).then(() => {
console.log("Delayed message after 1 second.");
});
3. async and await:
The async keyword is used to declare an asynchronous function, and the await keyword is used inside an async function to pause its execution until the Promise is resolved or rejected.
Example using async and await with a Promise:
const delay = (milliseconds) => {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
};
async function delayedMessage() {
await delay(1000);
console.log("Delayed message using async/await.");
}
delayedMessage();
4. Fetch API:
The Fetch API is used for making HTTP requests. It returns a Promise, allowing you to handle the response using then() and catch().
Example using Fetch API:
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error("Error fetching data:", error));
5. Error Handling:
When dealing with async operations, proper error handling is crucial to handle rejected Promises or errors that may occur during the async execution.
Example using Promise with error handling:
function fetchData() {
return new Promise((resolve, reject) => {
// Simulate an error
const errorOccurred = true;
if (errorOccurred) {
reject(new Error("Failed to fetch data."));
} else {
resolve("Data successfully fetched.");
}
});
}
fetchData()
.then((data) => console.log(data))
.catch((error) => console.error("Error:", error.message));
Promises
Promises are a fundamental concept in JavaScript used for handling asynchronous operations. They provide a clean and structured way to work with asynchronous code, making it easier to manage and avoid callback hell (nested callbacks). Promises were introduced in ECMAScript 6 (ES6) and have become a standard feature in modern JavaScript.
1. Promise States:
Promises can be in one of three states:
- Pending: The initial state when a Promise is created, and it represents that the asynchronous operation is still ongoing.
- Fulfilled (Resolved): The state when the asynchronous operation successfully completes, and the associated value (result) is available. This is typically handled by the
resolvefunction inside the Promise. - Rejected: The state when the asynchronous operation encounters an error or fails, and the associated reason (error) is available. This is typically handled by the
rejectfunction inside the Promise.
2. Creating a Promise:
A Promise is created using the Promise constructor, which takes an executor function as an argument. The executor function is called immediately when the Promise is created and is passed two callback functions: resolve and reject. Inside the executor function, you can perform your asynchronous operation, and then call resolve when the operation is successful, or reject when it fails.
const myPromise = new Promise((resolve, reject) => {
// Perform an asynchronous operation
// If successful:
resolve("Data fetched successfully");
// If an error occurs:
// reject(new Error("Failed to fetch data"));
});
3. Handling Promise Resolution:
You can handle the resolution of a Promise using the .then() method. It takes two optional callback functions as arguments: the first one is called when the Promise is fulfilled, and the second one is called when the Promise is rejected.
myPromise.then(
(result) => {
console.log("Fulfilled:", result);
},
(error) => {
console.log("Rejected:", error);
}
);
4. Chaining Promises:
Promises can be chained together using .then() to perform a sequence of asynchronous operations one after the other.
fetchDataFromServer()
.then((data) => processData(data))
.then((processedData) => saveData(processedData))
.then(() => console.log("Data saved successfully"))
.catch((error) => console.error("Error:", error));
5. Error Handling with .catch():
You can use the .catch() method at the end of a Promise chain to handle any errors that occur in the chain. If any Promise in the chain is rejected, the control will jump to the nearest .catch() block.
6. Promise.all():
Promise.all() is a utility method that takes an array of Promises as input and returns a new Promise that is fulfilled with an array of all resolved values when all the input Promises are fulfilled. If any of the input Promises are rejected, the Promise.all() will be rejected with the reason of the first rejected Promise.
const promise1 = fetchDataFromServer();
const promise2 = fetchDataFromAPI();
Promise.all([promise1, promise2])
.then((results) => {
console.log("All promises resolved:", results);
})
.catch((error) => {
console.error("One or more promises rejected:", error);
});
7. Promise.race():
Promise.race() is a utility method that takes an array of Promises as input and returns a new Promise that is settled (fulfilled or rejected) with the result of the first Promise (either fulfillment or rejection) that is settled in the input array.
const promise1 = fetchDataFromServer();
const promise2 = fetchDataFromAPI();
Promise.race([promise1, promise2])
.then((result) => {
console.log("The first promise settled:", result);
})
.catch((error) => {
console.error("The first promise rejected:", error);
});
Promise.allSettled
Promise.allSettled is a method introduced in ECMAScript 2020 (ES11) to handle multiple Promises simultaneously and get the result of all Promises, regardless of whether they are fulfilled or rejected. It returns a Promise that resolves with an array of objects representing the outcome of each input Promise. The returned objects contain a status property (either "fulfilled" or "rejected") and a value property (the fulfillment value or the reason for rejection).
Syntax:
Promise.allSettled(iterable);
Parameters:
iterable: An iterable (e.g., an array) containing Promises or other values to be converted to Promises.
Return Value:
- A Promise that resolves with an array of objects representing the outcome of each Promise in the input iterable.
Example:
const promise1 = Promise.resolve("Fulfilled Promise");
const promise2 = Promise.reject(new Error("Rejected Promise"));
const promise3 = new Promise((resolve) => setTimeout(() => resolve("Delayed Promise"), 1000));
Promise.allSettled([promise1, promise2, promise3])
.then((results) => {
console.log(results);
});
Output:
[
{ status: 'fulfilled', value: 'Fulfilled Promise' },
{ status: 'rejected', reason: Error: Rejected Promise at ... },
{ status: 'fulfilled', value: 'Delayed Promise' }
]
In this example, we have three Promises in the input array: promise1, promise2, and promise3. promise1 is fulfilled, promise2 is rejected, and promise3 is fulfilled after a 1-second delay.
Promise.allSettled waits for all Promises in the input array to settle (either fulfilled or rejected) and returns an array containing the outcome of each Promise as objects. Each object has two properties: status (with a value of "fulfilled" or "rejected") and either value (for fulfilled Promises) or reason (for rejected Promises).
The main advantage of using Promise.allSettled over Promise.all is that it doesn't short-circuit on the first rejected Promise. In other words, even if one or more Promises are rejected, Promise.allSettled will still wait for all Promises to settle before returning the result. This makes it useful in situations where you need to handle the result of multiple Promises regardless of their individual outcomes.
Promises in JavaScript simplify asynchronous code, making it more readable and manageable. By understanding and effectively using Promises, you can handle complex asynchronous operations with ease and create more robust and efficient JavaScript applications.
Threads, Concurrency, Parallelism
In JavaScript, understanding threads, concurrency, and parallelism is essential for optimizing performance, especially in the context of asynchronous and concurrent programming. Let's dive into each concept:
1. Threads:
A thread is the smallest unit of execution within a process. A process can have multiple threads running concurrently, each executing its own set of instructions. Threads allow for concurrent execution of tasks, and they share the same memory space within a process, making communication between threads more efficient.
However, in traditional JavaScript (in the browser or Node.js), JavaScript code runs in a single thread, often referred to as the "main thread." This single-threaded nature means that JavaScript code is executed sequentially, one operation at a time. This model is also known as "event-driven" or "non-blocking."
2. Concurrency:
Concurrency refers to the ability of a system to execute multiple tasks concurrently. In the context of JavaScript, concurrency is achieved through mechanisms like asynchronous programming and event loops. While JavaScript is single-threaded, it can still handle concurrent operations through non-blocking asynchronous functions, timers, and other APIs that offload tasks to the background and continue with the main execution.
3. Parallelism:
Parallelism, on the other hand, refers to the simultaneous execution of multiple tasks, where each task runs in its own separate thread or processor core. Unlike concurrency, parallelism requires multiple threads or cores to execute tasks simultaneously.
Concurrency vs. Parallelism in JavaScript:
JavaScript does not natively support parallelism due to its single-threaded nature. However, JavaScript can achieve concurrency using techniques like callbacks, Promises, and async/await, allowing multiple asynchronous tasks to be scheduled and executed efficiently.
To achieve parallelism in JavaScript, you would need to use Web Workers (in the browser) or worker threads (in Node.js). These features allow you to create separate threads to run computationally intensive tasks in parallel, taking advantage of multi-core processors.
Web Workers:
Web Workers are a browser feature that allows you to create separate threads to perform CPU-intensive tasks off the main thread. Web Workers can't access the DOM directly, but they can communicate with the main thread through message passing.
Example using a Web Worker:
// main.js
const worker = new Worker("worker.js");
worker.onmessage = (event) => {
console.log("Message from Worker:", event.data);
};
worker.postMessage("Start");
// worker.js
self.onmessage = (event) => {
console.log("Worker received:", event.data);
// Perform CPU-intensive task here
// Post the result back to the main thread
self.postMessage("Task completed");
};
Worker Threads in Node.js:
Node.js provides the worker_threads module to create separate threads for parallelism.
Example using worker threads in Node.js:
// main.js
const { Worker } = require("worker_threads");
const worker = new Worker("./worker.js");
worker.on("message", (message) => {
console.log("Message from Worker:", message);
});
worker.postMessage("Start");
// worker.js
const { parentPort } = require("worker_threads");
parentPort.on("message", (message) => {
console.log("Worker received:", message);
// Perform CPU-intensive task here
// Post the result back to the main thread
parentPort.postMessage("Task completed");
});
In summary, JavaScript is single-threaded, but it can achieve concurrency through asynchronous programming. To achieve parallelism, you can use Web Workers in the browser or worker threads in Node.js, which allow for separate threads to run computationally intensive tasks in parallel.