Promise.prototype.finally 适合隐藏骨架屏,因为它在 Promise settled 后必执行,不干扰返回值且不吞错误;需置于链末端或 try/finally 块中,避免竞态与错误覆盖。
因为 finally 不关心上一个 Promise 是 fulfilled 还是 rejected,只要状态 settled 就执行,天然契合“请求结束就收起骨架屏”的语义。它不接收参数,不会干扰链式返回值,也不会意外吞掉错误——这点比在 then 和 catch 里重复写隐藏逻辑更安全。
常见错误是把 finally 放错位置,比如写在 catch 后面却忘了 then 也可能抛错(例如 response.json() 解析失败),导致骨架屏卡住。正确做法是让 finally 紧跟在最终的 Promise 链末端:
showSkeleton();fetch('/api/data') .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then(data => render(data)) .catch(err => handleError(err)) .finally(() => hideSkeleton());
注意:如果中间用了 async/await,finally 不能直接链在 await 表达式后,得包一层 Promise:
showSkeleton();try { const res = await fetch('/api/data'); const data = await res.json(); render(data);} catch (err) { handleError(err);} finally { hideSkeleton();}
当多个并发请求共用同一个骨架屏开关(比如全局 isLoading 变量),直接在每个请求的 finally 里设 isLoading = false 会导致后完成的请求覆盖先完成的,骨架屏提前消失。解决方法:
pendingCount++,finally 里 pendingCount--,仅当 pendingCount === 0 时隐藏isPending,靠 UI 层统一控制显示逻辑这是最常被忽略的一点:finally 里的代码执行完后,原始 rejection 仍会继续冒泡。如果你在 finally 里写了 throw 或返回 rejected Promise,它会覆盖原错误;但什么也不做,就不会影响错误流向。所以以下写法是安全的:
fetch('/api/data') .then(handleSuccess) .catch(handleError) .finally(() => hideSkeleton()); // ✅ 原错误仍能被外层 try/catch 捕获
而这样会出问题:
.finally(() => { hideSkeleton(); return Promise.reject(new Error('finally broke it')); // ❌ 覆盖了原始错误})
实际项目中,只要 finally 体里只做副作用(如 DOM 更新、日志、清理定时器),就不需要担心错误流被破坏。