Master JavaScript Promises: Build Your Own From Scratch
This article explains what a JavaScript Promise is, why it solves callback‑hell in asynchronous code, details its three states, and walks through a complete hand‑written implementation—including the constructor, then method, resolvePromise logic, and how to verify compliance with the Promises/A+ test suite.
What is a Promise?
Syntax: Promise is a constructor that returns an object with state.
Function: Promise solves asynchronous functions and lets you react to results.
Specification: Promise is an object that has a then method (functions are objects in JS).
Why use a Promise?
Frontend developers often struggle with asynchronous requests, which lead to deeply nested callbacks (callback hell) that are hard to read and maintain.
Promises let you write asynchronous logic in a synchronous‑style flow, eliminating nested callbacks.
However, Promises also have drawbacks such as being non‑cancellable and lacking automatic error propagation when no callbacks are provided.
Promise states
A Promise can be in one of three states:
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'The state can only transition from pending to fulfilled or rejected once, and the result is stored.
Promise constructor
The Promise constructor accepts an executor function, which receives resolve and reject callbacks. These callbacks perform three tasks:
Change the promise state.
Store the value or reason.
Execute the onFulfilled / onRejected callbacks (the then method).
A minimal implementation looks like this:
class MyPromise {
constructor(executor) {
this.state = PENDING
this.value = null
this.reason = null
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = value => { /* ... */ }
const reject = reason => { /* ... */ }
try { executor(resolve, reject) } catch (e) { reject(e) }
}
}then method
The then method returns a new promise ( promise2) to enable chaining. It normalises non‑function arguments, handles the three possible current states, and queues callbacks when the promise is still pending.
then(onFulfilled, onRejected) {
let promise2
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onRejected === 'function' ? onRejected : r => { throw r }
if (this.state === FULFILLED) {
return promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try { let x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject) }
catch (e) { reject(e) }
})
})
}
if (this.state === REJECTED) {
return promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
try { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject) }
catch (e) { reject(e) }
})
})
}
if (this.state === PENDING) {
return promise2 = new MyPromise((resolve, reject) => {
this.onFulfilledCallbacks.push(value => {
try { let x = onFulfilled(value); resolvePromise(promise2, x, resolve, reject) }
catch (e) { reject(e) }
})
this.onRejectedCallbacks.push(reason => {
try { let x = onRejected(reason); resolvePromise(promise2, x, resolve, reject) }
catch (e) { reject(e) }
})
})
}
}resolvePromise function
This helper determines how promise2 should settle based on the value x returned from a callback. It handles:
Self‑reference cycles (reject with TypeError).
When x is a MyPromise (adopt its state, possibly recursively).
Thenable objects (objects with a then method) by calling then with a lock ( called) to ensure only one resolution.
Plain values (resolve directly).
function resolvePromise(promise2, x, resolve, reject) {
if (x === promise2) return reject(new TypeError('循环引用'))
if (x instanceof MyPromise) {
if (x.state === PENDING) {
x.then(y => resolvePromise(promise2, y, resolve, reject), r => reject(r))
} else {
x.then(resolve, reject)
}
return
}
if (x && (typeof x === 'function' || typeof x === 'object')) {
let called = false
try {
let then = x.then
if (typeof then === 'function') {
then.call(x, y => {
if (called) return
called = true
resolvePromise(promise2, y, resolve, reject)
}, r => {
if (called) return
called = true
reject(r)
})
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
return
}
resolve(x)
}Testing with Promises/A+ suite
To verify compliance, create a promise.js file containing the implementation, add a deferred helper, and run the official test suite via npm test after installing promises-aplus-tests.
Complete implementation
The article ends with the full source code of MyPromise, including the constructor, then, resolvePromise, and the deferred export needed for the test runner.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
