1. Callbacks
Callbacks involve passing a function as an argument to another function. They were one of the earliest and most commonly used methods for handling asynchronous operations in JavaScript.
Callbacks are not inherently asynchronous; they are simply a pattern for deferred execution.
Example:
function delayedTask(callback) {
setTimeout(() => {
if (callback) callback();
}, 2000);
}
delayedTask(() => {
console.log('Task completed');
});
In this example, setTimeout simulates a task that takes 2 seconds. Upon completion, the callback is invoked, allowing the main program to continue without blocking.
Advantages: Simple and easy to understand.
Disadvantages: Code can become difficult to read and maintain, leading to "callback hell" with deeply nested structures.
2. Event Listeners (Publish-Subscribe Pattern)
The publish-subscribe pattern establishes a one-to-many dependency between objects, so when one object changes state, all dependent objects are notified.
A basic example is attaching event handlers to DOM elements:
document.body.addEventListener('click', () => {
console.log('Clicked');
});
For custom events, you can implement a simple event emitter class:
class EventEmitter {
constructor() {
this._events = {};
}
subscribe(eventName, handler) {
if (!this._events[eventName]) {
this._events[eventName] = [];
}
this._events[eventName].push(handler);
}
emit(eventName, ...args) {
if (!this._events[eventName]) return;
this._events[eventName].forEach(handler => handler(...args));
}
}
Usage:
const emitter = new EventEmitter();
emitter.subscribe('dataReady', (arg1, arg2) => {
console.log(arg1, arg2);
});
function main() {
console.log('Main program running');
setTimeout(() => {
emitter.emit('dataReady', 'Parameter 1', 'Parameter 2');
}, 1000);
}
main();
Advantages: Promotes modularity and allows for optimized event handling.
Disadvantages: Can make the program overly event-driven, complicating the flow, and requires boilerplate code for registrasion and triggering.
3. Promises
Introduced in ES6, Promises provide a cleaner way to handle asynchronous operations by allowing you to write asynchronous code in a more synchronous style, avoiding callback hell.
A Promise has three states: pending, fulfilled, or rejected. Once settled, its state cannot change.
Example:
function simulateAsync(delay) {
return new Promise(resolve => {
setTimeout(() => resolve(delay + 500), delay);
});
}
function stepOne(delay) {
console.log(`Step 1 with delay ${delay}`);
return simulateAsync(delay);
}
function stepTwo(delay) {
console.log(`Step 2 with delay ${delay}`);
return simulateAsync(delay);
}
function stepThree(delay) {
console.log(`Step 3 with delay ${delay}`);
return simulateAsync(delay);
}
function executeSteps() {
let initialDelay = 0;
stepOne(initialDelay)
.then(result => stepTwo(result))
.then(result => stepThree(result))
.then(finalResult => {
console.log(`Final result: ${finalResult}`);
});
}
executeSteps();
Adventages: Enables chaining and improves readability by flattening nested callbacks.
Disadvantages: Promises execute immediately upon creation and cannot be cancelled. Errors must be handled with callbacks to prevent silent failures.
4. Generators
Generator functions can pause and resume execution, making them useful for managing asynchronous flow control.
Example:
function* sequenceGenerator() {
console.log('Start');
yield 'First yield';
console.log('Middle');
yield 'Second yield';
console.log('End');
return 'Finished';
}
const iterator = sequenceGenerator();
console.log(iterator.next()); // { value: 'First yield', done: false }
console.log(iterator.next()); // { value: 'Second yield', done: false }
console.log(iterator.next()); // { value: 'Finished', done: true }
Advantages: Provides fine-grained control over execution flow.
Disadvantages: Requires an external executor (like next()) to manage the generator, which can be cumbersome for pure asynchronous tasks.
5. Async/Await
Async/await, introduced in ES2017, is built on Promises and offers a syntax that makes asynchronous code look and behave more like synchronous code.
asyncfunctions always return a Promise.awaitpauses execution until the Promise resolves, then returns the resolved value.
Example:
async function runAsyncTasks() {
let delay1 = 0;
let delay2 = await stepOne(delay1);
let delay3 = await stepTwo(delay2);
let finalDelay = await stepThree(delay3);
console.log(`Result: ${finalDelay}`);
}
runAsyncTasks();
Advantages: Clean, readable syntax with built-in error handling via try/catch.
Disadvantages: Improper use of await can lead to performance issues due to blocking behavior if not used with concurrent operations.
Summary
This article reviewed five methods for handling asynchronous operations in JavaScript: callbacks, event listeners, Promises, Generators, and async/await. Each has its use cases, with async/await often being the preferred choice for modern applications due to its clarity and ease of use.