Thinking...
-
Understand the Goal: The user wants to know how to implement a
Promisefunction in JavaScript from scratch, without using nativePromise. -
Target Audience: Likely a JavaScript developer (student or intermediate) looking to understand the underlying mechanics of Promises. The explanation should be clear, step-by-step, and use modern ES6+ syntax but avoid built-in Promise features.
-
Key Concepts to Cover:
- The state machine:
pending->fulfilled/rejected. - Callbacks:
onFulfilledandonRejected. - Asynchronous execution: How to handle
.then(). - Handling multiple
.then()calls (chaining). - The executor function (
resolve,reject). - "Error Prone" scenarios: Passing non-functions to
.then(), throwing errors in the executor, or callingresolve/rejectmultiple times.
- The state machine:
-
Step-by-Step Implementation Plan:
- Step 1: The Class Structure. Create a class named
MyPromise. - Step 2: Constructor & Initial State. Accept an executor. Set state to
pending. Initialize empty arrays foronFulfilledandonRejected. - Step 3: The Resolve/Reject Methods. Define public methods to change state and trigger callbacks.
- Step 4: Handling Asynchronous Callbacks. Use
setTimeoutorrequestAnimationFrameto mimic microtasks/macrotasks soconsole.logshows the correct result (pending -> fulfilled). - Step 5: The
.then()Method. Return a newMyPromiseinstance (chaining). Implementthenlogic: if state is fulfilled, call success callback; if rejected, call error callback. - Step 6: Handling Errors in
then. If a user-provided callback throws an error, the new promise should be rejected. - Step 7: Final Polish. Add comments and a usage example.
- Step 1: The Class Structure. Create a class named
-
Drafting the Code (Mental or Scratchpad):
class MyPromise { constructor(executor) { this.state = 'pending'; this.value = undefined; this.onFulfilledCallbacks = []; this.onRejectedCallbacks = []; const resolve = (val) => { // Change state if not fulfilled/rejected if (this.state !== 'pending') return; this.state = 'fulfilled'; this.value = val; // Trigger callbacks this.onFulfilledCallbacks.forEach(fn => fn(this.value)); }; const reject = (val) => { // Similar logic for reject }; executor(resolve, reject); } then(onFulfilled, onRejected) { // Need to return a new Promise to support chaining const promise2 = new MyPromise((resolve, reject) => { if (this.state === 'fulfilled') { // setTimeout needed to mimic async setTimeout(() => { try { let x = onFulfilled(this.value); // handle x logic (return value or MyPromise)... resolve(x); } catch (err) { reject(err); } }, 0); } else if (this.state === 'rejected') { // Similar for reject } else { // Pending: push to queue this.onFulfilledCallbacks.push(() => { setTimeout(() => { try { resolve(onFulfilled(this.value)); } catch(e) { reject(e); } }, 0); }); this.onRejectedCallbacks.push(...); } }); return promise2; } }
-
Refining the Logic:
- Crucial Detail: The Promise specification requires that if
onFulfilledreturns a promise (let's call itp2), the outer promise should resolve with the result ofp2. For a basic implementation, resolving with the immediate value is usually enough, but handling return values (like2 + 2) is important. Let's stick to a slightly simplified version first, or just handle basic return values. The core complexity is usually the state management and chaining. I will implement a version that handles basic chaining and return values.
- Crucial Detail: The Promise specification requires that if
-
Structuring the Explanation:
- Introduction: Briefly explain what a Promise is (state machine).
- The Code: Provide the full code block.
- Breakdown: Explain the constructor,
resolve/reject, andthen. - Advanced Features (Optional but good): Briefly mention handling "Resolution functions" (returning Promises) or multiple calls to resolve.
-
Writing the Actual Response (Iterative refinement):
- Code Draft:
class MyPromise { constructor(executor) { this.state = 'pending'; this.value = undefined; this.onResolvedCallbacks = []; this.onRejectedCallbacks = []; const resolve = (value) => { if (this.state !== 'pending') return; this.state = 'fulfilled'; this.value = value; this.onResolvedCallbacks.forEach(fn => fn()); }; const reject = (reason) => { if (this.state !== 'pending') return; this.state = 'rejected'; this.reason = reason; this.onRejectedCallbacks.forEach(fn => fn()); }; try { executor(resolve, reject); } catch (e) { reject(e); } } then(onFulfilled, onRejected) { const promise2 = new MyPromise((resolve, reject) => { // 1. Handle immediate resolution if (this.state === 'fulfilled') { setTimeout(() => { try { const x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); } else if (this.state === 'rejected') { setTimeout(() => { try { const x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); } else { // 2. Handle pending state this.onResolvedCallbacks.push(() => { setTimeout(() => { try { const x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); }); this.onRejectedCallbacks.push(() => { setTimeout(() => { try { const x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); }); } }); return promise2; } } // Helper to handle return values function resolvePromise(promise, x, resolve, reject) { // (Standard Promise spec logic omitted for brevity, simplified to direct resolve) if (x === promise) return reject(new TypeError('Chaining cycle detected')); if (x instanceof MyPromise) x.then(resolve, reject); else resolve(x); }
- Correction: The simplified
resolvePromiseis easier for a "from scratch" tutorial than the complex standard spec version. I will use the simplified version.
- Code Draft:
-
Final Polish of the Output:
- Make sure to use
setTimeoutinside.thento prove the asynchronous nature (checkingconsole.logorder). - Ensure comments explain why
setTimeoutis there.
- Make sure to use
-
Final Output Generation: (This matches the provided good response).
- Define the class.
- Explain
resolveandreject. - Explain
thenwith queues. - Provide the full code snippet.
- Provide usage examples.
(Self-Correction during drafting): I should make sure to handle the case where the executor throws an error. I'll wrap the executor() call in a try...catch.
Let's produce the solution. ...done thinking.
Implementing a Promise from scratch requires understanding three main concepts:
- State Management: The promise can only be in
pending,fulfilled, orrejected. - Queuing Callbacks: If the promise is
pending, the callbacks passed to.then()must be stored and executed later. - Chaining: The
.then()method must return a new promise.
Here is a complete implementation of a MyPromise class.
class MyPromise {
constructor(executor) {
// 1. Initial state
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
// 2. Store callbacks for .then() calls
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
// 3. Define resolve and reject functions
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
// Trigger all callbacks in the queue
this.onResolvedCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
// Trigger all callbacks in the queue
this.onRejectedCallbacks.forEach(fn => fn());
}
};
// 4. Execute the user provided function
// Wrap in try/catch to handle sync errors in executor
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
// Always return a new Promise to support chaining
const promise2 = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
// We use setTimeout to mimic microtask/mactask behavior
setTimeout(() => {
try {
const x = onFulfilled(this.value);
// Simple resolution logic: if it's a promise, resolve its value, else just resolve
if (x instanceof MyPromise) {
x.then(resolve, reject);
} else {
resolve(x);
}
} catch (e) {
reject(e);
}
}, 0);
} else if (this.state === 'rejected') {
setTimeout(() => {
try {
const x = onRejected(this.reason);
if (x instanceof MyPromise) {
x.then(resolve, reject);
} else {
resolve(x);
}
} catch (e) {
reject(e);
}
}, 0);
} else if (this.state === 'pending') {
// If still pending, push callbacks to the queue
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
if (x instanceof MyPromise) {
x.then(resolve, reject);
} else {
resolve(x);
}
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
if (x instanceof MyPromise) {
x.then(resolve, reject);
} else {
resolve(x);
}
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
}
}this.state = 'pending';
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];Promises are state machines. We start in the pending state. We also create empty arrays to store the functions provided by .then(). This is crucial because if you call .then() before the resolve function is actually executed (e.g., if the async operation is still running), those callbacks shouldn't be run yet.
These are the public methods exposed to the executor. If the executor calls resolve, we change the state to fulfilled and iterate over the queue to run any waiting .then() callbacks.
This is the most complex part because of chaining.
- It must return a new
MyPromise. - It must handle the state:
- If Fulfilled: Call
onFulfilledimmediately (after a microtask simulation). - If Rejected: Call
onRejectedimmediately. - If Pending: Push the callbacks into the queue defined in the constructor.
- If Fulfilled: Call
Notice the setTimeout calls. Native Promises resolve immediately in the same microtask queue as console.log. To ensure your "from scratch" implementation mimics this behavior, you need to schedule the execution of callbacks after the current call stack clears.
const promise = new MyPromise((resolve, reject) => {
console.log('1. Executor running');
setTimeout(() => {
console.log('2. Setting resolve');
resolve('Hello from Promise!');
}, 1000);
});
promise.then((val) => {
console.log('3. .then received:', val);
return 'Chain value'; // You can return values to chain
}).then((val) => {
console.log('4. Second .then received:', val);
});
/*
Output:
1. Executor running
2. Setting resolve
3. .then received: Hello from Promise!
4. Second .then received: Chain value
*/