Mastering the Command Pattern for Async Flows
In this lesson, you will learn how to encapsulate asynchronous tasks as objects to gain total control over when, how, and why your code executes.
The Problem: Fragmented Logic
When you trigger an asynchronous operation directly (like calling a database function inside a route handler), that logic is "lost" to the system. You can’t easily pause it, log it, or roll it back if something goes wrong. The Command Pattern solves this by turning a request into a standalone object that holds all the data needed to perform an action.
Encapsulating Tasks
Think of a Command object as a "to-do slip." Instead of just executing code, you create a class that contains an execute() method. Because the method is stored inside an object, you can pass it around, put it in a queue, or even store it in a history list for "Undo" functionality.
Here is a simple example of a command that writes to a database:
class SaveUserCommand {
constructor(db, userData) {
this.db = db;
this.userData = userData;
}
async execute() {
console.log("Saving user...");
return await this.db.users.save(this.userData);
}
async undo() {
console.log("Rolling back user save...");
return await this.db.users.delete(this.userData.id);
}
}
Why This Matters for Async
By wrapping your async flows in objects, you unlock powerful capabilities:
- Queuing: You can push command objects into an array and process them one by one, managing your system's load.
- Logging: Since the command is an object, you can easily log exactly what operation was requested before it even starts.
- Undo: If an operation fails midway through an async chain, you can call the
undo()method of previous commands to return your system to a clean state.
Instead of writing "fire-and-forget" functions, you are now creating "manageable tasks" that live within the boundaries of your application’s state.
Key Takeaway: By transforming asynchronous operations into Command objects, you turn raw function calls into trackable, reversible, and manageable units of work.