How to Build Real Promise Chains: From Basics to Full Implementation

This article walks through the step‑by‑step creation of a JavaScript Promise implementation, explains why true chaining requires returning a new Promise from then, demonstrates mock asynchronous calls, and provides detailed code examples and execution logs to illustrate the complete chain behavior.

vivo Internet Technology
vivo Internet Technology
vivo Internet Technology
How to Build Real Promise Chains: From Basics to Full Implementation

Series Overview

The tutorial series aims to deepen understanding of JavaScript Promises by progressively implementing them, using flowcharts, code examples, and animations.

Part 1 – Basic implementation

Part 2 – Promise chaining

Part 3 – Prototype methods

Part 4 – Static methods

Minimal Promise Implementation

class Promise {
    callbacks = [];
    state = 'pending'; // added state
    value = null;    // store result
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        if (this.state === 'pending') {
            this.callbacks.push(onFulfilled);
        } else {
            onFulfilled(this.value);
        }
        return this; // returns the same instance
    }
    _resolve(value) {
        this.state = 'fulfilled'; // change state
        this.value = value;       // store result
        this.callbacks.forEach(fn => fn(value));
    }
}

This version supports basic resolution but cannot produce true chainable behavior because then returns the same Promise instance.

Why Returning a New Promise Is Required

When each then call returns this, all callbacks share the same result, preventing the progressive transformation of values. True chaining requires then to return a fresh Promise whose resolution depends on the previous one.

function getUserId(url) {
    return new Promise(resolve => {
        http.get(url, id => resolve(id));
    });
}
getUserId('some_url')
    .then(id => getNameById(id))
    .then(name => getCourseByName(name))
    .then(course => getCourseDetailByCourse(course));

Each onFulfilled returns a different value, so a new Promise must be created for the next step.

Full Chainable Implementation

class Promise {
    callbacks = [];
    state = 'pending';
    value = null;
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        return new Promise(resolve => {
            this._handle({ onFulfilled: onFulfilled || null, resolve });
        });
    }
    _handle(callback) {
        if (this.state === 'pending') {
            this.callbacks.push(callback);
            return;
        }
        if (!callback.onFulfilled) {
            callback.resolve(this.value);
            return;
        }
        const ret = callback.onFulfilled(this.value);
        callback.resolve(ret);
    }
    _resolve(value) {
        this.state = 'fulfilled';
        this.value = value;
        this.callbacks.forEach(cb => this._handle(cb));
    }
}

Key points:

then creates and returns a new Promise, forming the basis of serial chaining.

The pair onFulfilled and the new Promise’s resolve are stored together in the current Promise’s callback queue, linking the current and next Promise.

If onFulfilled is omitted, the next Promise receives the current value unchanged.

Mock Asynchronous Request

/**
 * Simulate an asynchronous request
 * @param {*} url   Request URL
 * @param {*} s     Delay in seconds
 * @param {*} callback Callback after response
 */
const mockAjax = (url, s, callback) => {
    setTimeout(() => {
        callback(url + ' async request took ' + s + ' seconds');
    }, 1000 * s);
};

Demo 1 – Simple Chain

new Promise(resolve => {
    mockAjax('getUserId', 1, result => resolve(result));
}).then(result => console.log(result));

Log output shows constructor, then registration, resolution, and the final printed result.

Demo 2 – Transforming Values

new Promise(resolve => {
    mockAjax('getUserId', 1, result => resolve(result));
}).then(result => {
    console.log(result);
    return 'prefix:' + result;
}).then(exResult => console.log(exResult));

The second then receives the transformed value.

Demo 3 – Equivalent Synchronous Version

new Promise(resolve => {
    mockAjax('getUserId', 1, result => resolve(result));
}).then(result => {
    console.log(result);
    const exResult = 'prefix:' + result;
    console.log(exResult);
    const finalResult = exResult + ':suffix';
    console.log(finalResult);
});

This shows that when only the first step is asynchronous, the later steps can be written synchronously.

Handling Returned Promises

If onFulfilled returns another Promise, the current Promise must adopt its state. The modified _resolve checks for thenable objects:

_resolve(value) {
    if (value && (typeof value === 'object' || typeof value === 'function')) {
        const then = value.then;
        if (typeof then === 'function') {
            then.call(value, this._resolve.bind(this));
            return;
        }
    }
    this.state = 'fulfilled';
    this.value = value;
    this.callbacks.forEach(cb => this._handle(cb));
}

Demo 4 – Chaining Promises Returned from onFulfilled

const pUserId = new Promise(resolve => {
    mockAjax('getUserId', 1, result => resolve(result));
});
const pUserName = new Promise(resolve => {
    mockAjax('getUserName', 2, result => resolve(result));
});

pUserId.then(id => {
    console.log(id);
    return pUserName; // return another Promise
}).then(name => console.log(name));

Logs demonstrate that the second then waits for pUserName to fulfill before executing.

Conclusion

The article provides a complete, step‑by‑step implementation of true Promise chaining, covering basic resolution, callback queue management, handling of thenable values, and practical demos with mock asynchronous calls. Understanding these internals helps developers write reliable asynchronous code and avoid common pitfalls.

Promise chain diagram
Promise chain diagram
JavaScripttutorialAsyncImplementationPromiseChain
vivo Internet Technology
Written by

vivo Internet Technology

Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.

0 followers
Reader feedback

How this landed with the community

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.