Mastering JavaScript Promises: From Scratch to Full A+ Implementation
This article walks you through building a fully functional Promise library in JavaScript, starting with a basic implementation, then adding asynchronous handling, chaining, state management, error handling, and finally aligning with the Promise/A+ specification, complete with code examples and testing guidance.
Overview
This guide explains how to implement a Promise library in JavaScript from scratch, covering the basic functionality, asynchronous execution, chaining, state management, error handling, and compliance with the Promise/A+ specification.
Version 1 – Basic Promise
Start with a simple class that stores callbacks, provides a
thenmethod to collect them, and resolves them when
_resolveis called.
<code>class Promise {
callbacks = []
constructor(fn) {
fn(this._resolve.bind(this))
}
then(onFulfilled) {
this.callbacks.push(onFulfilled)
}
_resolve(val) {
const me = this
me.callbacks.forEach(fn => fn(val))
}
}</code>Version 2 – Asynchronous Execution and Chaining
Add a
setTimeoutin
_resolveto ensure callbacks run asynchronously and return
thisfrom
thento enable simple chaining.
<code>class Promise {
callbacks = []
constructor(fn) {
fn(this._resolve.bind(this))
}
then(onFulfilled) {
this.callbacks.push(onFulfilled)
return this
}
_resolve(val) {
setTimeout(() => {
this.callbacks.forEach(fn => fn(val))
})
}
}</code>Version 3 – Proper Chainable Promises
Introduce internal state (
pending,
fulfilled), store the resolved value, and make
thenreturn a new Promise that resolves with the result of the previous callback, achieving true chaining.
<code>class Promise {
callbacks = []
value = null
state = 'pending'
constructor(fn) {
fn(this._resolve.bind(this))
}
then(onFulfilled = null) {
const me = this
return new Promise(resolve => {
me._handle({ onFulfilled, resolve })
})
}
_handle(cbObj) {
if (this.state === 'pending') {
this.callbacks.push(cbObj)
return
}
if (!cbObj.onFulfilled) {
this._resolve(this.value)
return
}
const val = cbObj.onFulfilled(this.value)
cbObj.resolve(val)
}
_resolve(val) {
setTimeout(() => {
this.value = val
this.state = 'fulfilled'
this.callbacks.forEach(cb => this._handle(cb))
})
}
}</code>Version 3.1 – Handling Returned Promises
Detect if a callback returns a promise or thenable and bind its
thento the internal
resolve, allowing nested asynchronous operations.
<code>_resolve(val) {
const me = this
if (val && (typeof val === 'object' || typeof val === 'function')) {
const then = val.then
if (then && typeof then === 'function') {
then.call(val, me._resolve.bind(me))
return
}
}
setTimeout(() => {
me.value = val
me.state = 'fulfilled'
me.callbacks.forEach(cb => me._handle(cb))
})
}</code>Version 4 – Full A+ Compliance
Add
rejectedstate,
rejectmethod, and error handling in
then. The implementation now follows the Promise/A+ spec, including protection against multiple state changes and handling of self‑resolution errors.
<code>class Promise {
callbacks = []
value = null
reason = null
state = 'pending'
constructor(fn) {
try {
fn(this._resolve.bind(this), this._reject.bind(this))
} catch (e) {
this._reject(e)
}
}
then(onFulfilled = null, onRejected = null) {
const me = this
return new Promise((resolve, reject) => {
me._handle({ onFulfilled, onRejected, resolve, reject })
})
}
_handle(cbObj) {
if (this.state === 'pending') {
this.callbacks.push(cbObj)
return
}
const cb = this.state === 'fulfilled' ? cbObj.onFulfilled : cbObj.onRejected
const next = this.state === 'fulfilled' ? cbObj.resolve : cbObj.reject
if (!cb) {
next(this.state === 'fulfilled' ? this.value : this.reason)
return
}
try {
const val = cb(this.state === 'fulfilled' ? this.value : this.reason)
cbObj.resolve(val)
} catch (e) {
cbObj.reject(e)
}
}
_resolve(val) {
if (this.state !== 'pending') return
if (this === val) {
return this._reject(new TypeError('cannot return the same promise object from onfulfilled or onrejected callback.'))
}
if (val && (typeof val === 'object' || typeof val === 'function')) {
const then = val.then
if (then && typeof then === 'function') {
then.call(val, this._resolve.bind(this), this._reject.bind(this))
return
}
}
setTimeout(() => {
this.value = val
this.state = 'fulfilled'
this._execute()
})
}
_reject(reason) {
if (this.state !== 'pending') return
setTimeout(() => {
this.reason = reason
this.state = 'rejected'
this._execute()
})
}
_execute() {
this.callbacks.forEach(cb => this._handle(cb))
}
}</code>The article also provides links to the Promise/A+ spec, a reference implementation by 阮一峰, and instructions for running the
promises-aplus-testssuite against the custom implementation.
37 Mobile Game Tech Team
37 Mobile Game Tech Team
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.