在这里插入图片描述
@[toc]( 前端打工人必看:Promise.then()链式调用3天吃透(含踩坑血泪史))

前端打工人必看:Promise.then()链式调用3天吃透(含踩坑血泪史)

说实话,Promise这玩意儿我到现在有时候还会写错。不是不懂原理,就是那种"脑子会了手不会"的感觉,你懂的。今天咱们不整那些虚的,就把我这些年踩过的坑、流过的泪、砸过的键盘,统统掏出来给你看。


先唠唠为啥这玩意儿老让人头大

刚入行那会儿被回调地狱支配的恐惧,谁懂啊

我记得特别清楚,2018年我刚入行第二个月,老大丢给我一个需求:先登录拿token,然后用token换用户信息,再用用户信息查订单列表。听起来很简单对吧?我当时是这么写的:

// 警告:以下代码包含令人不适的内容,请谨慎观看
login(username, password, function(token) {
    getUserInfo(token, function(userInfo) {
        getOrderList(userInfo.userId, function(orders) {
            renderOrders(orders, function() {
                console.log('终于完了');
            });
        });
    });
});

写完我还挺得意,觉得代码挺整齐的啊,都是向右缩进的。结果老大路过我工位,看了一眼屏幕,沉默了三秒,说:“你这代码,像楼梯,还是那种旋转楼梯。”

我当时没get到点,直到三天后需求变了,要在中间加一步校验用户状态。我盯着那个向右漂移了快半个屏幕的代码,陷入了深深的自我怀疑。这就是传说中的回调地狱(Callback Hell),也叫末日金字塔(Pyramid of Doom)。名字挺中二的,但痛苦是真实的。

后来我知道有Promise这玩意儿,兴冲冲地去看教程。MDN文档、阮一峰的博客、各种掘金文章,看完我觉得自己都懂了——不就是个状态机嘛,pending、fulfilled、rejected,then用来处理成功,catch处理失败,finally不管成败都会执行。简单!

然后我一写代码就废。

// 我以为的链式调用
fetchUser()
    .then(user => fetchOrders(user.id))
    .then(orders => console.log(orders));

// 实际跑起来的结果
// Uncaught TypeError: Cannot read property 'then' of undefined

我盯着那个undefined看了十分钟,想不通啊。fetchOrders明明返回了数据,怎么就undefined了?后来才发现,我少写了一个return。对,就是那么简单一个return,让我加班到十点。

明明看了教程,一写代码就废

我觉得Promise难学,很大一部分原因是教程和现实的鸿沟。教程里的例子都是这样的:

const promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('success'), 1000);
});

promise
    .then(value => console.log(value))
    .catch(error => console.error(error));

干净、整洁、没有业务逻辑干扰。但真实项目里呢?你要处理参数校验、错误码判断、数据转换、loading状态管理,还要考虑网络超时、重试机制、取消请求……这些东西一掺和进去,Promise链就像毛线团一样越缠越乱。

还有那个经典的面试题:"Promise.then()返回什么?"我当年面试某大厂时就被问到了。我脱口而出:"返回Promise啊!"面试官微微一笑:"那如果then里的回调返回一个普通值呢?"我愣了一下,说:"那……那也返回Promise吧?"面试官又笑了:"对,但值会被包装成Promise。那如果返回的是Promise呢?"我开始冒汗:“那就……直接返回那个Promise?”

其实我当时是蒙对的。但这种似懂非懂的状态,写代码时就会翻车。比如你以为返回了一个值,实际上是返回了一个Promise,然后你又.then了一下,结果发现值变成了[object Promise],这种酸爽谁试谁知道。

面试官最爱问Promise,问完还问你then返回啥,真的栓Q

说到面试,Promise简直是前端面试的"必考曲目"。我总结了一下面试官的套路:

第一问:Promise有几种状态?pending、fulfilled、rejected,送分题。

第二问:Promise怎么解决回调地狱?链式调用,送分题。

第三问:then方法返回什么?开始上强度了。

第四问:如果then里抛出异常会怎样?被下一个catch捕获,还行。

第五问:Promise.all和Promise.race的区别?前者全部成功才成功,后者有一个出结果就出结果。

第六问:手写一个Promise?卒。

我经历过最狠的一次面试,面试官让我用Promise实现一个请求并发控制,限制最多同时发起3个请求。我当时脑子一片空白,满脑子都是then().then().then(),完全不知道从哪里下手。后来回家一查,要用到递归或者队列,跟单纯的链式调用完全不是一回事。

所以啊,Promise这东西,看起来简单,水很深。咱们今天就把then()这个最核心的方法掰开了揉碎了讲,争取让你三天内从"好像懂了"变成"真能写了"。


Promise.then()到底是个啥东西

别整那些官方定义,说白了就是异步操作完成后的回调注册器

MDN上怎么说的来着?“The then() method returns a Promise. It takes up to two arguments: callback functions for the success and failure cases of the Promise.” 这话没错,但太官方了,看完还是不知道咋用。

我的理解是:then就是一个"等会儿再说"的登记处。你把一个函数交给它,说:"等这个Promise有结果了,帮我执行一下这个函数。"至于这个函数什么时候执行、执行几次、返回值怎么处理,then都帮你安排得明明白白。

// 最基础的用法
const p = new Promise((resolve) => {
    setTimeout(() => resolve('饭做好了'), 1000);
});

p.then(msg => {
    console.log(msg); // 1秒后输出:饭做好了
});

这里的关键点是:then不会立刻执行你给的函数。它先把函数存起来,等Promise的状态从pending变成fulfilled(或者rejected),再把这个函数扔到微任务队列里。所以即使你写的是同步resolve,then里的代码也是异步执行的:

const p = Promise.resolve('立即完成');

p.then(value => console.log(value));
console.log('这行先执行');

// 输出顺序:
// 这行先执行
// 立即完成

这个特性很重要,很多人踩坑就是因为以为then是同步的。比如你想在then里修改一个变量,然后立即使用这个变量,结果发现还没改过来,就是因为then里的代码还没执行呢。

then方法执行完返回的还是Promise,这才是能链式调用的关键

这是链式调用的核心秘密。咱们来看一段代码:

const p1 = fetch('/api/user');
const p2 = p1.then(response => response.json());
const p3 = p2.then(data => console.log(data));

console.log(p1 === p2); // false
console.log(p2 === p3); // false
console.log(p1 instanceof Promise); // true
console.log(p2 instanceof Promise); // true
console.log(p3 instanceof Promise); // true

每一个then都返回一个新的Promise对象,不是原来的那个。这就是为什么可以一直.then().then().then()下去,就像接力赛跑一样,一棒接一棒。

但这里有个容易混淆的点:新的Promise的状态和值,取决于then里回调函数的返回值。这个咱们下一节细说,先记住这个结论。

每个then就像快递中转站,数据一站一站往下传

我喜欢把Promise链想象成快递物流。每个then就是一个中转站,包裹(数据)从上一个站点运过来,经过处理(回调函数),再发往下一个站点。

fetch('/api/user')  // 起点:发货
    .then(res => res.json())  // 中转站1:拆包装
    .then(user => user.name)  // 中转站2:取名字
    .then(name => name.toUpperCase())  // 中转站3:转大写
    .then(upperName => console.log(upperName));  // 终点:签收

如果某个中转站出了问题(抛出错误或者返回rejected的Promise),包裹就会被送到最近的catch站点(错误处理),后面的then站点就都不会收到了。

这个比喻的好处是,你可以直观地理解为什么链式调用能避免回调地狱。传统的回调是"俄罗斯套娃",一个套一个,越套越深;Promise链是"流水线",每个环节处理完就传给下一个,扁平化、易读、易维护。


链式调用then().then()的核心逻辑拆解

第一个then处理完的数据怎么跑到第二个then里去

这是新手最容易困惑的地方。咱们写个最简单的例子:

Promise.resolve(1)
    .then(val => {
        console.log('第一个then:', val); // 1
        return val + 1;
    })
    .then(val => {
        console.log('第二个then:', val); // 2
        return val * 2;
    })
    .then(val => {
        console.log('第三个then:', val); // 4
    });

第一个then收到的是1,返回2;第二个then收到的是2,返回4;第三个then收到的是4。数据就这么一站一站传下去了。

但这里有个大坑:如果你忘记写return,下一个then收到的就是undefined。这是我踩过最多的坑,没有之一。

Promise.resolve(1)
    .then(val => {
        console.log('第一个then:', val); // 1
        val + 1; // 注意:没有return!
    })
    .then(val => {
        console.log('第二个then:', val); // undefined,惊不惊喜?
    });

为什么?因为JavaScript函数默认返回undefined。then看到你没return,就以为你要返回undefined,于是包装成Promise.resolve(undefined)传给下一个then。

我debug这种问题的经验是:在then的第一行就写return,哪怕暂时不知道返回什么,先写个return null占个位。这样能避免80%的链式断裂问题。

返回值是普通值还是Promise,下游接收的完全不一样

这是链式调动的精髓,也是面试最常考的。咱们分四种情况讨论:

情况1:返回普通值

Promise.resolve('start')
    .then(val => {
        return '普通字符串'; // 返回普通值
    })
    .then(val => {
        console.log(val); // 普通字符串
        console.log(typeof val); // string
    });

then会自动把这个普通值包装成fulfilled的Promise,所以下一个then能直接拿到这个值,不用unwrap。

情况2:返回Promise

Promise.resolve('start')
    .then(val => {
        return Promise.resolve('我是Promise'); // 返回Promise
    })
    .then(val => {
        console.log(val); // 我是Promise
        // 注意:这里拿到的是resolve的值,不是Promise对象本身
    });

then会"展开"这个Promise,把它的最终值传给下一个then。这叫做Promise的展开(unwrap)或者穿透(penetrate)

情况3:返回thenable对象(有then方法的对象)

const thenable = {
    then(resolve, reject) {
        resolve(42);
    }
};

Promise.resolve('start')
    .then(() => thenable) // 返回thenable对象
    .then(val => {
        console.log(val); // 42
    });

then会把这个对象当成Promise处理,调用它的then方法,等它resolve了再把值传下去。这个特性很少用,但看源码或者一些库的时候会碰到。

情况4:抛出错误

Promise.resolve('start')
    .then(() => {
        throw new Error('出错了!');
    })
    .then(val => {
        console.log('这行不会执行');
    })
    .catch(err => {
        console.log(err.message); // 出错了!
    });

then里的回调抛出错误,相当于返回了一个rejected的Promise,会直接跳到最近的catch。这也是为什么建议每个then后面都跟个catch,或者最后统一catch。

then里面不写return的话,下一个then拿到的就是undefined,血泪教训

这个我必须单独拎出来再说一遍,因为真的太容易踩坑了。来看一个真实场景:

// 需求:先获取用户ID,再获取用户详情
function getUser() {
    return fetch('/api/user')
        .then(res => res.json());
}

function getUserDetail(userId) {
    return fetch(`/api/user/${userId}`)
        .then(res => res.json());
}

// 错误的写法
getUser()
    .then(user => {
        getUserDetail(user.id); // 忘记return!
    })
    .then(detail => {
        console.log(detail); // undefined
    });

// 正确的写法
getUser()
    .then(user => {
        return getUserDetail(user.id); // 记得return
    })
    .then(detail => {
        console.log(detail); // 真正的用户详情
    });

我第一次写这种代码的时候,盯着undefined看了半小时,死活想不通。getUserDetail明明发起了请求,network面板也能看到响应,为什么then里拿不到?

后来才恍然大悟:getUserDetail返回的是一个Promise,但你没把这个Promise返回给then,then就以为你要返回undefined。于是下一个then收到的就是undefined,而那个真正的Promise在原地飘着,没人处理它的结果。

这种错误在代码审查时也很难发现,因为语法上完全没问题,逻辑上也看起来对。我现在的习惯是:只要then里有异步操作,第一时间检查有没有return


这玩意儿好使在哪,又有哪些坑

代码扁平化了,不用一层层回调嵌套,看着清爽

这是Promise最大的卖点。咱们来对比一下回调地狱和Promise链:

回调地狱版:

login(credentials, (err, token) => {
    if (err) {
        handleError(err);
        return;
    }
    getUserInfo(token, (err, user) => {
        if (err) {
            handleError(err);
            return;
        }
        getOrders(user.id, (err, orders) => {
            if (err) {
                handleError(err);
                return;
            }
            renderOrders(orders, (err) => {
                if (err) {
                    handleError(err);
                    return;
                }
                console.log('完成');
            });
        });
    });
});

Promise链版:

login(credentials)
    .then(token => getUserInfo(token))
    .then(user => getOrders(user.id))
    .then(orders => renderOrders(orders))
    .then(() => console.log('完成'))
    .catch(err => handleError(err));

代码行数其实差不多,但结构完全不一样了。Promise链是纵向发展,一眼就能看到数据流动的路径;回调地狱是横向发展,每一层都要处理错误,代码越缩越靠右,最后超出屏幕。

而且Promise链的错误处理可以统一放到最后,不用每层都写if (err)。这个咱们下面细说。

错误可以统一捕获,不用每个回调都写try-catch

这也是Promise的一大优势。在回调地狱里,每个回调都可能出错,你都得处理。Promise链里,任何一个then抛出错误,都会被最近的catch捕获:

Promise.resolve(1)
    .then(() => {
        throw new Error('第一步出错');
    })
    .then(() => {
        // 这行不会执行
        console.log('第二步');
    })
    .then(() => {
        // 这行也不会执行
        console.log('第三步');
    })
    .catch(err => {
        console.log('捕获到错误:', err.message); // 第一步出错
    });

错误会像冒泡一样,沿着链往上走,直到被catch捕获。这意味着你可以把错误处理逻辑集中写在一处,代码更干净。

但这里有个坑:如果你在catch里处理了错误,没有重新抛出,后面的then会继续执行

Promise.resolve(1)
    .then(() => {
        throw new Error('出错了');
    })
    .catch(err => {
        console.log('处理错误:', err.message);
        // 没有return,默认返回undefined
    })
    .then(val => {
        console.log('继续执行:', val); // 继续执行: undefined
    });

如果你希望错误被捕获后停止执行,需要在catch里重新抛出:

Promise.resolve(1)
    .then(() => {
        throw new Error('出错了');
    })
    .catch(err => {
        console.log('处理错误:', err.message);
        throw err; // 重新抛出,中断链式调用
    })
    .then(val => {
        // 这行不会执行
        console.log('继续执行:', val);
    })
    .catch(err => {
        console.log('再次捕获:', err.message);
    });

坑就是then太多了一层一层找数据,调试的时候想砸键盘

Promise链也不是完美的。当链太长的时候,调试起来真的很痛苦。比如这种:

fetchUser()
    .then(user => fetchProfile(user.id))
    .then(profile => fetchSettings(profile.settingId))
    .then(settings => fetchPermissions(settings.role))
    .then(permissions => fetchMenu(permissions.accessLevel))
    .then(menu => render(menu))
    .catch(err => console.error(err));

如果最后render出来的菜单不对,你得一层一层往上查:是fetchMenu的参数错了?还是permissions结构不对?还是settings里没有role?每一层都可能出问题,console.log得加好几处。

我现在的做法是:超过3个then的链,考虑用async-await重构,或者把中间步骤拆成有意义的变量

// 重构后
async function initApp() {
    try {
        const user = await fetchUser();
        const profile = await fetchProfile(user.id);
        const settings = await fetchSettings(profile.settingId);
        const permissions = await fetchPermissions(settings.role);
        const menu = await fetchMenu(permissions.accessLevel);
        render(menu);
    } catch (err) {
        console.error(err);
    }
}

或者:

// 或者保留Promise但加中间变量
fetchUser()
    .then(user => {
        console.log('User:', user); // 方便调试
        return fetchProfile(user.id);
    })
    .then(profile => {
        console.log('Profile:', profile);
        return fetchSettings(profile.settingId);
    })
    // ... 以此类推

链式调用一旦中间某个环节挂了,后面全废,得好好处理reject

这是Promise链的另一个特性:一旦某个then返回rejected的Promise(或者抛出错误),整个链就断了,后面的then都不会执行,直到被catch捕获

Promise.resolve(1)
    .then(() => Promise.reject('中间出错'))
    .then(() => console.log('这行不会执行'))
    .then(() => console.log('这行也不会执行'))
    .catch(err => console.log('捕获:', err)); // 捕获: 中间出错

这在某些场景下是优点(错误不会静默失败),但在某些场景下是坑。比如你想并行执行几个操作,即使其中一个失败,其他的也要继续,就不能直接用Promise链。

还有更隐蔽的坑:如果你在then里调用了一个返回Promise的函数,但没有return,这个Promise的错误就捕获不到

Promise.resolve(1)
    .then(() => {
        // 忘记return!
        fetch('/api/data'); // 这个Promise的错误没人处理
    })
    .catch(err => {
        // 捕获不到上面的错误
        console.log('捕获:', err);
    });

如果fetch失败了,错误会变成unhandled promise rejection,可能在你意想不到的时候崩溃。所以再次强调:异步操作记得return


实际项目里我是怎么用的

接口请求套娃场景,先拿token再拿用户信息再拿订单列表

这是最常见的场景。假设我们有一个电商后台,登录后要展示用户的订单列表:

// auth.js - 登录相关
function login(username, password) {
    return fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    })
    .then(res => {
        if (!res.ok) throw new Error('登录失败');
        return res.json();
    })
    .then(data => {
        // 保存token到localStorage
        localStorage.setItem('token', data.token);
        return data.token;
    });
}

// user.js - 用户相关
function getUserInfo(token) {
    return fetch('/api/user/info', {
        headers: { 'Authorization': `Bearer ${token}` }
    })
    .then(res => {
        if (!res.ok) throw new Error('获取用户信息失败');
        return res.json();
    });
}

// order.js - 订单相关
function getOrderList(userId) {
    return fetch(`/api/orders?userId=${userId}`)
        .then(res => {
            if (!res.ok) throw new Error('获取订单失败');
            return res.json();
        });
}

// 组合使用
function initDashboard(username, password) {
    return login(username, password)
        .then(token => {
            console.log('登录成功,token:', token);
            return getUserInfo(token);
        })
        .then(user => {
            console.log('获取用户信息:', user.name);
            return getOrderList(user.id);
        })
        .then(orders => {
            console.log(`获取到${orders.length}个订单`);
            return orders;
        })
        .catch(err => {
            console.error('初始化失败:', err.message);
            // 根据错误类型做不同处理
            if (err.message.includes('登录')) {
                showLoginError();
            } else {
                showToast(err.message);
            }
            throw err; // 继续抛出,让上层知道出错了
        });
}

// 使用
initDashboard('admin', '123456')
    .then(orders => renderOrderTable(orders))
    .catch(() => {
        // 最终错误处理
        showErrorPage();
    });

这个例子展示了几个最佳实践:

  1. 每个功能模块独立封装,返回Promise
  2. 组合时链式调用,逻辑清晰
  3. 每层都有错误处理,但主要逻辑集中在最后
  4. 使用throw创建可追踪的错误

文件上传分步处理,上传-校验-存储-返回结果一条龙

文件上传通常需要多个步骤,每个步骤都可能失败,用Promise链很适合:

class FileUploader {
    constructor(file) {
        this.file = file;
        this.progress = 0;
    }

    // 步骤1:校验文件
    validate() {
        return new Promise((resolve, reject) => {
            // 检查文件大小
            if (this.file.size > 10 * 1024 * 1024) {
                reject(new Error('文件大小超过10MB限制'));
                return;
            }
            
            // 检查文件类型
            const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
            if (!allowedTypes.includes(this.file.type)) {
                reject(new Error('不支持的文件类型'));
                return;
            }
            
            console.log('文件校验通过');
            resolve(this.file);
        });
    }

    // 步骤2:计算MD5(用于断点续传和秒传)
    calculateMD5() {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = (e) => {
                const spark = new SparkMD5.ArrayBuffer();
                spark.append(e.target.result);
                const md5 = spark.end();
                console.log('MD5计算完成:', md5);
                resolve({ file: this.file, md5 });
            };
            reader.onerror = () => reject(new Error('文件读取失败'));
            reader.readAsArrayBuffer(this.file);
        });
    }

    // 步骤3:检查是否可以秒传
    checkFastUpload(md5) {
        return fetch(`/api/file/check?md5=${md5}`)
            .then(res => res.json())
            .then(data => {
                if (data.exists) {
                    console.log('文件已存在,秒传成功');
                    return { fastUpload: true, url: data.url };
                }
                return { fastUpload: false, md5 };
            });
    }

    // 步骤4:上传文件(如果不是秒传)
    upload({ fastUpload, md5, url }) {
        if (fastUpload) {
            return Promise.resolve({ url, status: 'fast' });
        }

        return new Promise((resolve, reject) => {
            const formData = new FormData();
            formData.append('file', this.file);
            formData.append('md5', md5);

            const xhr = new XMLHttpRequest();
            
            // 进度监听
            xhr.upload.onprogress = (e) => {
                if (e.lengthComputable) {
                    this.progress = (e.loaded / e.total) * 100;
                    this.onProgress && this.onProgress(this.progress);
                }
            };

            xhr.onload = () => {
                if (xhr.status === 200) {
                    const response = JSON.parse(xhr.responseText);
                    resolve({ url: response.url, status: 'uploaded' });
                } else {
                    reject(new Error('上传失败'));
                }
            };

            xhr.onerror = () => reject(new Error('网络错误'));
            xhr.open('POST', '/api/file/upload');
            xhr.send(formData);
        });
    }

    // 步骤5:保存到数据库
    saveToDB({ url, status }) {
        return fetch('/api/file/record', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                filename: this.file.name,
                size: this.file.size,
                url: url,
                uploadType: status,
                uploadTime: new Date().toISOString()
            })
        })
        .then(res => res.json())
        .then(record => {
            console.log('记录保存成功:', record.id);
            return { ...record, url };
        });
    }

    // 执行完整流程
    start() {
        return this.validate()
            .then(() => this.calculateMD5())
            .then(({ md5 }) => this.checkFastUpload(md5))
            .then(result => this.upload(result))
            .then(result => this.saveToDB(result))
            .then(finalResult => {
                console.log('上传流程全部完成');
                return finalResult;
            })
            .catch(err => {
                console.error('上传流程出错:', err.message);
                throw err;
            });
    }

    // 进度回调
    onProgress(callback) {
        this.onProgress = callback;
        return this;
    }
}

// 使用示例
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (e) => {
    const file = e.target.files[0];
    if (!file) return;

    const uploader = new FileUploader(file);
    
    uploader
        .onProgress((progress) => {
            document.getElementById('progressBar').style.width = `${progress}%`;
        })
        .start()
        .then(result => {
            alert(`上传成功!文件地址:${result.url}`);
        })
        .catch(err => {
            alert(`上传失败:${err.message}`);
        });
});

这个例子展示了如何用Promise链组织复杂的异步流程。每个步骤都是独立的函数,返回Promise,通过链式调用组合起来。好处是:

  • 步骤清晰,一眼能看出上传流程
  • 每个步骤可以单独测试
  • 错误统一处理,不用每个步骤都写try-catch
  • 支持秒传优化,提升用户体验

多个异步任务串行执行,比如先清理缓存再拉取新数据

有时候我们需要按顺序执行多个异步任务,后面的依赖前面的结果:

// 数据同步管理器
class DataSyncManager {
    constructor() {
        this.cache = new Map();
    }

    // 清理缓存
    clearCache() {
        return new Promise((resolve) => {
            console.log('正在清理缓存...');
            setTimeout(() => {
                this.cache.clear();
                console.log('缓存已清空');
                resolve();
            }, 500);
        });
    }

    // 获取基础数据
    fetchBaseData() {
        return fetch('/api/base-data')
            .then(res => res.json())
            .then(data => {
                console.log('基础数据获取完成');
                this.cache.set('base', data);
                return data;
            });
    }

    // 获取配置(依赖基础数据)
    fetchConfig(baseData) {
        const version = baseData.version;
        return fetch(`/api/config?v=${version}`)
            .then(res => res.json())
            .then(config => {
                console.log('配置获取完成');
                this.cache.set('config', config);
                return config;
            });
    }

    // 获取用户权限(依赖配置)
    fetchPermissions(config) {
        const features = config.enabledFeatures;
        return fetch('/api/permissions', {
            method: 'POST',
            body: JSON.stringify({ features })
        })
            .then(res => res.json())
            .then(permissions => {
                console.log('权限获取完成');
                this.cache.set('permissions', permissions);
                return permissions;
            });
    }

    // 预加载资源(依赖权限)
    preloadResources(permissions) {
        const resources = permissions.accessibleResources;
        const promises = resources.map(url => 
            fetch(url).then(res => res.blob())
        );
        
        return Promise.all(promises)
            .then(blobs => {
                console.log(`预加载了${blobs.length}个资源`);
                this.cache.set('resources', blobs);
                return blobs;
            });
    }

    // 执行完整同步流程
    sync() {
        return this.clearCache()
            .then(() => this.fetchBaseData())
            .then(baseData => this.fetchConfig(baseData))
            .then(config => this.fetchPermissions(config))
            .then(permissions => this.preloadResources(permissions))
            .then(() => {
                console.log('所有数据同步完成');
                return {
                    success: true,
                    cache: this.cache
                };
            })
            .catch(err => {
                console.error('同步失败:', err);
                return {
                    success: false,
                    error: err.message
                };
            });
    }
}

// 使用
const syncManager = new DataSyncManager();
document.getElementById('syncBtn').addEventListener('click', () => {
    syncManager.sync().then(result => {
        if (result.success) {
            alert('数据同步成功!');
        } else {
            alert(`同步失败:${result.error}`);
        }
    });
});

这个例子展示了串行执行多个依赖任务的场景。每个步骤都依赖前一个步骤的结果,用Promise链可以很好地表达这种依赖关系。注意preloadResources里用了Promise.all,这是并行处理多个独立请求的技巧,和串行的Promise链结合使用,可以灵活控制异步流程。


代码跑不起来时的排查套路

控制台先看Promise状态,pending还是rejected一目了然

调试Promise的第一步,是确认Promise当前的状态。虽然JavaScript没有直接提供获取Promise状态的方法,但我们可以通过一些小技巧来查看:

// 给Promise加标签,方便调试
function logPromise(promise, label) {
    promise
        .then(value => {
            console.log(`[${label}] ✅ Fulfilled:`, value);
            return value;
        })
        .catch(reason => {
            console.log(`[${label}] ❌ Rejected:`, reason);
            throw reason;
        });
    return promise;
}

// 使用
const userPromise = fetchUser();
logPromise(userPromise, '获取用户');

const detailPromise = userPromise.then(u => getDetail(u.id));
logPromise(detailPromise, '获取详情');

更好的方法是使用Chrome DevTools的调试功能。在Sources面板中,你可以看到Promise的状态,以及链式调用的完整堆栈。

每个then后面都加个catch定位问题出在第几环

当链式调用出错时,定位具体是哪个then出了问题很关键。我的做法是:临时在每个then后面加catch, pinpoint错误位置

fetchUser()
    .then(user => {
        console.log('Step 1 - User:', user);
        return getProfile(user.id);
    })
    .catch(err => {
        console.error('Error in Step 1:', err); // 定位第一步
        throw err;
    })
    .then(profile => {
        console.log('Step 2 - Profile:', profile);
        return getSettings(profile.id);
    })
    .catch(err => {
        console.error('Error in Step 2:', err); // 定位第二步
        throw err;
    })
    .then(settings => {
        console.log('Step 3 - Settings:', settings);
        return render(settings);
    })
    .catch(err => {
        console.error('Error in Step 3:', err); // 定位第三步
        throw err;
    });

这样控制台会精确显示错误发生在第几步。定位到问题后,再把中间的catch去掉,保留最后的统一错误处理。

用async-await包装一下,断点调试比纯then链友好太多

这是我最推荐的调试方法。如果你发现Promise链很难debug,可以临时用async-await重写,设置断点:

// 原来的Promise链
function fetchData() {
    return fetchUser()
        .then(user => getProfile(user.id))
        .then(profile => getSettings(profile.id))
        .then(settings => render(settings));
}

// 临时改成async-await用于调试
async function fetchDataDebug() {
    try {
        const user = await fetchUser(); // 在这里打断点
        console.log('User:', user);
        
        const profile = await getProfile(user.id); // 或者这里
        console.log('Profile:', profile);
        
        const settings = await getSettings(profile.id); // 或者这里
        console.log('Settings:', settings);
        
        return render(settings);
    } catch (err) {
        console.error('Error:', err);
        throw err;
    }
}

在async函数里,你可以像调试同步代码一样设置断点,查看每一步的变量值,比.then().then()的链式调用直观多了。调通后再改回Promise链(如果需要的话)。

常见翻车现场:忘记return、错误没捕获、数据类型搞混

我总结了Promise链最常见的三种错误:

翻车现场1:忘记return

// ❌ 错误
fetchUser().then(user => {
    fetchProfile(user.id); // 没有return!
}).then(profile => {
    // profile是undefined
});

// ✅ 正确
fetchUser().then(user => {
    return fetchProfile(user.id);
}).then(profile => {
    // profile是正常数据
});

翻车现场2:错误没捕获导致unhandled rejection

// ❌ 错误
fetchUser().then(user => {
    return riskyOperation(user); // 可能抛出错误
}); // 没有catch,错误没人管

// ✅ 正确
fetchUser().then(user => {
    return riskyOperation(user);
}).catch(err => {
    console.error('操作失败:', err);
});

翻车现场3:数据类型搞混,该返回Promise的返回了普通值

// ❌ 错误:以为返回了Promise,实际返回了undefined
function updateUser(user) {
    fetch('/api/user', {
        method: 'PUT',
        body: JSON.stringify(user)
    }).then(res => res.json()); // 没有return!
}

// ✅ 正确
function updateUser(user) {
    return fetch('/api/user', {
        method: 'PUT',
        body: JSON.stringify(user)
    }).then(res => res.json());
}

让代码更骚的一些小技巧

用变量把中间结果存一下,别全靠链式传,调试能省一半时间

链式调用的问题在于,中间结果都被"吞"在链里了,想看某个中间值很麻烦。我的做法是:把重要的中间结果存到外部变量,既方便调试,也方便后续复用:

let currentUser = null;
let userPermissions = null;

fetchUser()
    .then(user => {
        currentUser = user; // 存起来!
        console.log('当前用户:', currentUser);
        return fetchPermissions(user.id);
    })
    .then(permissions => {
        userPermissions = permissions; // 存起来!
        console.log('用户权限:', userPermissions);
        // 后面可能还要用到currentUser
        if (userPermissions.isAdmin) {
            return fetchAllData();
        } else {
            return fetchUserData(currentUser.id);
        }
    });

这样你可以在控制台随时查看currentUseruserPermissions,不用在每个then里console.log。而且如果后面的逻辑需要用到前面的数据,也不用通过参数一层层传。

错误处理统一放最后,别每个then都写一遍catch

虽然前面说调试时可以每个then都加catch,但生产代码应该保持简洁。推荐的做法是:只在关键点处理特定错误,其他错误统一在最后catch

fetchUser()
    .then(user => {
        if (!user.isActive) {
            // 特定业务错误,立即处理
            throw new Error('用户账号已被禁用');
        }
        return user;
    })
    .then(user => fetchProfile(user.id))
    .then(profile => render(profile))
    .catch(err => {
        // 统一错误处理
        if (err.message === '用户账号已被禁用') {
            showDisabledMessage();
        } else if (err.name === 'NetworkError') {
            showNetworkError();
        } else {
            showGenericError(err.message);
        }
        console.error('操作失败:', err);
    });

这样代码既简洁,又能处理各种错误情况。

复杂的链式调用拆成独立函数,可读性直接起飞

当Promise链超过3个then时,考虑拆分成有意义的函数:

// ❌ 太长,难以阅读
function initApp() {
    return checkAuth()
        .then(auth => fetchUser(auth.token))
        .then(user => fetchConfig(user.id))
        .then(config => loadPlugins(config.plugins))
        .then(plugins => initRouter(plugins))
        .then(router => render(router))
        .catch(handleError);
}

// ✅ 拆分成步骤函数
function initApp() {
    return checkAuth()
        .then(fetchUserData)
        .then(initializeConfig)
        .then(setupPlugins)
        .then(configureRouter)
        .then(renderApp)
        .catch(handleError);
}

function fetchUserData(auth) {
    return fetchUser(auth.token);
}

function initializeConfig(user) {
    return fetchConfig(user.id);
}

function setupPlugins(config) {
    return loadPlugins(config.plugins);
}

function configureRouter(plugins) {
    return initRouter(plugins);
}

function renderApp(router) {
    return render(router);
}

这样每个函数只做一件事,命名清晰,测试也方便。

能用async-await就别硬写then链,2026年了别跟自己过不去

虽然本文讲的是Promise.then(),但我必须说:2026年了,能用async-await就别硬写then链。async-await本质上是Promise的语法糖,但写起来更像同步代码,更易读、易调试、易维护。

对比:

// Promise链
function getData() {
    return fetchUser()
        .then(user => fetchOrders(user.id))
        .then(orders => orders.filter(o => o.status === 'pending'))
        .then(pendingOrders => pendingOrders.map(o => o.total))
        .then(totals => totals.reduce((a, b) => a + b, 0));
}

// async-await
async function getData() {
    const user = await fetchUser();
    const orders = await fetchOrders(user.id);
    const pendingOrders = orders.filter(o => o.status === 'pending');
    const totals = pendingOrders.map(o => o.total);
    return totals.reduce((a, b) => a + b, 0);
}

async-await版本一眼就能看出数据转换的流程,而Promise链版本需要仔细看每个then的返回值。

当然,Promise链在某些场景下还是有优势的,比如需要并行处理或者复杂的错误处理时。但大部分情况下,async-await是更好的选择。


最后说点掏心窝子的

学Promise别光看,动手写,写崩了再修,修多了就熟了

我学Promise的经历是这样的:看教程觉得懂了 → 写代码翻车 → 看更多教程 → 继续翻车 → 突然有一天就顿悟了。

Promise难的不是概念,是实践中的各种边界情况。比如:

  • 在then里调用另一个Promise,要不要return?
  • 错误在什么时候会被吞掉?
  • 怎么取消一个Promise?(标准Promise不支持取消,得用AbortController或者包装一层)

这些问题光看教程是体会不到的,必须亲手写,写错了debug,debug多了就有感觉了。

我建议的学习路径:

  1. 先理解Promise的基本概念(状态、then、catch)
  2. 把现有的回调代码改写成Promise链
  3. 故意制造一些错误,看Promise怎么表现
  4. 学习Promise.all、Promise.race等静态方法
  5. 最后学async-await

链式调用不是越长越好,超过3个then考虑重构

我见过这样的代码:

fetchA()
    .then(a => fetchB(a))
    .then(b => fetchC(b))
    .then(c => fetchD(c))
    .then(d => fetchE(d))
    .then(e => fetchF(e))
    .then(f => fetchG(f))
    .then(g => render(g))
    .catch(err => console.error(err));

七个then!这种代码维护起来是噩梦。超过3个then,就该考虑:

  • 用async-await重构
  • 把中间步骤合并成有意义的函数
  • 检查是否真的需要这么多步骤(有些步骤也许可以并行)

记住一点:then里面返回值决定下一个then收到啥,这句背下来能救急

如果你只能记住一点,记住这个:then回调的返回值,会被包装成Promise,传给下一个then

  • 返回普通值 → 下一个then收到这个值
  • 返回Promise → 下一个then收到这个Promise的resolve值
  • 抛出错误 → 跳到最近的catch
  • 什么都不返回 → 下一个then收到undefined

理解了这个,Promise链的大部分问题都能解决。

实在搞不定就async-await,不丢人,能跑就行

最后,如果你看了这么多,还是觉得Promise链很难搞,那就用async-await吧。真的,不丢人。代码首先是给人看的,其次才是给机器执行的。async-await写起来更自然,出错概率更低,团队协作时也更容易被理解。

当然,理解Promise的原理还是必要的,因为async-await底层就是Promise,而且很多库和API还是返回Promise。但日常开发中,没必要硬写复杂的then链,怎么爽怎么来。


写到这里,感觉把这些年踩过的坑都倒出来了。Promise这东西,说难不难,说简单也不简单,关键是多练。希望这篇文章能帮到你,至少让你在遇到undefined或者Uncaught (in promise)的时候,能更快地定位问题。

加油,打工人!💪

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!


专栏系列(点击解锁) 学习路线(点击解锁) 知识定位
《微信小程序相关博客》 持续更新中~ 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》 持续更新中~ AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》 《前端基础入门三大核心之html相关博客》 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》 Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》 持续更新中~ 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》 持续更新中~ Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》 持续更新中~ SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》 持续更新中~ 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》 持续更新中~ 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》 持续更新中~ 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》 持续更新中~ 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》 持续更新中~ 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐