-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
194: Building a Simple Promise Implementation from Scratch
I've noticed a recurring theme when talking to developers about Promises: many of you treat them as if they are the cause of asynchronicity. You might think that wrapping a function in a new Promise() is what actually makes the code run in the background. It's a common mental shortcut, but it's fundamentally wrong.
Promises aren't async magic; they are state machines
If you write new Promise(resolve => { console.log('Hi'); resolve(); }), that 'Hi' prints immediately. There is no "magic thread" being spawned. The Promise itself doesn't make things asynchronous; it just provides a standardized way to track the result of an asynchronous operation that is already happening elsewhere (like a fetch call or a setTimeout).
Think of a Promise as a receipt at a deli. The receipt isn't the sandwich; it's just a piece of paper that says, "I'm currently Pending. Eventually, I will either be Fulfilled (here is your sandwich) or Rejected (we ran out of rye bread)." If you try to eat the receipt, you're not getting any lunch.
Building the internal state tracker
To build our own version, we need a class that tracks three things: the current state, the resulting value, and a list of functions to call once the state changes. I'll call our implementation MyPromise.
class MyPromise {
constructor(executor) {
this.state = 'PENDING';
this.value = undefined;
this.handlers = [];
// We define resolve and reject inside the constructor
// so they have access to this specific instance.
const resolve = (value) => this._transition('FULFILLED', value);
const reject = (error) => this._transition('REJECTED', error);
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
_transition(state, value) {
if (this.state !== 'PENDING') return; // Promises are immutable once settled
this.state = state;
this.value = value;
// Once settled, we fire off all the queued .then() callbacks
this.handlers.forEach(handler => handler());
}
}
Notice that _transition is where the "truth" of the Promise lives. I added a check to ensure that if a Promise is already fulfilled, it can't suddenly become rejected. That's a core part of the spec.
Handling the .then() queue
The trickiest part is .then(). If the Promise is already finished, the callback should run immediately. If it's still pending, we need to store that callback in our handlers array and wait.
In a real JS engine, .then() callbacks are pushed to the Microtask Queue, meaning they run after the current script finishes but before the browser repaints. For our simple version, we'll use queueMicrotask to mimic this behavior so our Promise doesn't behave synchronously.
then(onFulfilled) {
return new MyPromise((resolve) => {
const executeHandler = () => {
queueMicrotask(() => {
if (this.state === 'FULFILLED') {
const result = onFulfilled(this.value);
resolve(result);
} else {
// For simplicity, we are ignoring rejection handling here
resolve();
}
});
};
if (this.state === 'FULFILLED') {
executeHandler();
} else {
this.handlers.push(executeHandler);
}
});
}
I've returned a new MyPromise inside the then method. This is why you can chain Promises. Each .then() creates a new "receipt" that depends on the previous one being fulfilled.
Now, let's test it with something that is actually asynchronous, like a timer. If we wrap a setTimeout in our MyPromise, we can see the state machine in action: it starts pending, the timer expires, resolve() is called, the state transitions to fulfilled, and the handlers are flushed.
const p = new MyPromise((resolve) => {
setTimeout(() => resolve("Sandwich is ready!"), 1000);
});
p.then(val => console.log(val));
console.log("I'm waiting...");
// Output:
// "I'm waiting..."
// (1 second pause)
// "Sandwich is ready!"📋 Practical Task
Implement a Promise.all() Clone for MyPromise
Now that you have the MyPromise class working, your task is to create a static method called all(). This method should take an array of MyPromise instances and return a single MyPromise that resolves only when all the input promises have resolved.
Requirements:
- The returned promise should resolve to an array of values in the same order as the input array.
- If any of the input promises reject, the main promise should reject immediately.
- Do not use the built-in
Promise.all(); use yourMyPromiseimplementation.
// Expected usage:
MyPromise.all([
new MyPromise(res => setTimeout(() => res(1), 100)),
new MyPromise(res => setTimeout(() => res(2), 200)),
new MyPromise(res => setTimeout(() => res(3), 50))
]).then(results => {
console.log(results); // Should log [1, 2, 3] after 200ms
});There are no comments for now.