Fundamentals 15 min read

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.

37 Mobile Game Tech Team
37 Mobile Game Tech Team
37 Mobile Game Tech Team
Mastering JavaScript Promises: From Scratch to Full A+ Implementation

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

then

method to collect them, and resolves them when

_resolve

is 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

setTimeout

in

_resolve

to ensure callbacks run asynchronously and return

this

from

then

to 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

then

return 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

then

to 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

rejected

state,

reject

method, 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-tests

suite against the custom implementation.

Promise chain diagram
Promise chain diagram
State diagram
State diagram
Promise flow
Promise flow
JavaScriptTutorialAsyncImplementationPromiseA+ Specification
37 Mobile Game Tech Team
Written by

37 Mobile Game Tech Team

37 Mobile Game Tech Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.