> For the complete documentation index, see [llms.txt](https://anjia1.gitbook.io/web/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://anjia1.gitbook.io/web/js/runtime/promise/best-practices.md).

# 最佳实践

## 1. 最佳实践

> A good rule of thumb is to always either return or terminate promise chains, and as soon as you get a new promise, return it immediately, to flatten things.

### 1.1 总 `return`

在写 `then()` 的回调函数时，记得 return 结果，否则下一个 `then()` 的回调是拿不到前面的处理结果的，也就是说 promise chain 断了。

尤其是当 `onFulfilled/onRejected` 里定义了一个新 promise 时，更要将其返回，否则该 promise 就没地方跟踪它的 settlement 了。与此同时，下一个 `then()` 也会被提前调用，而且 fulfilled 的 `value` 是 `undefined`（即上面提到的“promise chain 断了”）。在这种情况下，我们就有了两条独立的 promise chain，其中，里面的 chain 没人管，外面的 chain 还断了。

```javascript
doSomething()
    .then((url) => {
        // 没有 return
        fetch(url)  // 1. 它的执行结果到底是什么，没人知道
    })
    .then((result) => {
        console.log(result) // 2. undefined，因为上一个 handler 没返回任何值
    })
```

所以，在写 `onFulfilled/onRejected` 回调函数时，最好总是 return 结果。而且一旦遇到了 promise，务必将其 return，并 defer 到下一个 `then` 里再处理其结果。

### 1.2 总用 `catch` 终止

如果一个 promise chain 没有用 `catch` 终止，就有可能出现 `uncaught errors`。

![](/files/DdekaiVswTwMGzZXO4lO)

![](/files/6zOCodbkaFY9hoUsadyB)

### 1.3 不随便嵌套 promise

promise chain 不要随便 nest（嵌套），尽量保持 flat。

nesting 是一种控制结构，用来限制 `catch` 语句的范围。正确使用 nest 可以提高 error 恢复的精度。比如：

```js
doSomethingCritical()
    .then((result) =>
        doSomethingOptional(result)
            .then((optionalResult) => doSomethingExtraNice(optionalResult))
            .catch((e) => { })
    ) // Ignore if optional stuff fails. proceed.
    .then(() => moreCriticalStuff())
    .catch((e) => console.error(`Critical failure: ${e.message}`))
```

下面，对比看看 promise chain 的两种写法：

```javascript
const listOfIngredients = []

// 1. nest
doSomething()
    .then((url) => fetch(url).then((res) => res.json()).then((data) => { listOfIngredients.push(data) }))
    .then(() => { console.log(listOfIngredients) })
    
// 2. flat（更推荐把 promise chain 展开）
//    一旦得到了一个新的 promise，就立即返回，以 flatten
doSomething()
    .then((url) => fetch(url))
    .then((res) => res.json())
    .then((data) => {
        listOfIngredients.push(data)
    })
    .then(() => {
        console.log(listOfIngredients)
    })
```

### 1.4 举例

看看下面的代码，有哪几处错误？

{% code lineNumbers="true" %}

```js
doSomething()
    .then((result) => {
        doSomethingElse(result)
            .then((newResult) => doThirdThing(newResult))
    })
    .then(() => doFourthThing())
```

{% endcode %}

<details>

<summary>点击查看答案</summary>

有三处：

1. 第 2 行的 promise 没有 return
   * not chain things together properly，通常发生在创建了一个新 promise 但忘了将其 return
   * 后果就是：the chain is broken 或者说有了两个独立的 chains racing
2. 不必要的嵌套
3. 没有用 `catch` 终止 chain

修正后的代码如下：

```js
doSomething()
    .then((result) => doSomethingElse(result))
    .then((newResult) => doThirdThing(newResult))
    .then(() => doFourthThing())
    .catch(error => { console.log(error) })
```

这样，我们就有了一个带 error 处理的 deterministic（确定性的）chain。

</details>

## 2. 错误处理

### 2.1 `catch()`

`catch()` 会捕获 `thrown` 语句抛出的异常，以及代码的 errors。

对于相同的错误处理逻辑，可以只写一个 `catch` 语句。比如：

{% code lineNumbers="true" %}

```js
doSomething()
    .then((result) => doSomethingElse(result))
    .then((newResult) => doThirdThing(newResult))
    .then((finalResult) => console.log(`Got the final result: ${finalResult}`))
    .catch(failureCallback)
// 由于前面的三个 then() 都没有 onRejected 参数
// 所以会把 rejection reason 一路透传到第四个 catch()
```

{% endcode %}

### 2.2 全局 event

如果 promise 的 rejection 事件没有被任何 handler 处理，那么它就会冒泡到 call stack 的顶部，而 host（宿主）需要把 error 显示出来。

在 web 中，当 promise 被 rejected 时，以下两个 events 之一会被发送到 global scope，它们的发送时机分别是：

* `unhandledrejection`（未处理的拒绝）：当 promise 被 rejected 了但没有可用的 rejection handler 时
* `rejectionhandled`（拒绝处理）：当 handler 被附加到（已引起 `unhandledrejection` 事件的）rejected promise 上时

在 Node.js 中，是监听 `unhandledRejection` 事件。

```javascript
// process.on() 可避免 error 输出到 console（控制台）
process.on('unhandledRejection', (reason, promise) => {
    // Add code here to examine the 'promise' and 'reason' values
})
```

### 2.3 reject 写法

以下语句都可以触发 promise 的 reject 的逻辑：

<pre class="language-javascript"><code class="lang-javascript">reject("oh, no!")
// reject(new Error("oh, no!"))

// throw ('oh, no!')
throw new Error('oh, no!')  // 注意：异步 throw，就和 uncaught error 一样

return Promise.reject('oh, no!')
<strong>// return Promise.reject(new Error('oh, no!'))
</strong></code></pre>

其中，被注释的语句是相对不推荐的。推荐的做法是：

1. 正常返回用 `reject()` 或 `Promise.reject()`。具体选哪个，取决于返回值的类型：
   * 若返回值的类型是非 promise，则用前者，以便直接进入 rejected 状态的逻辑
   * 若要返回 promise 类型，则用后者，毕竟它内部新建一个 promise 还是要花内存和时间的
2. 出错用 `throw new Error()` 显式抛出错误，好处是：
   * 有语义，表示出错了
   * 能保留 stack trace（堆栈跟踪）等有价值的信息
   * 可以避免 `catch` 部分做类型检查——判断是 String 还是 Error
   * 性能方面的小顾虑：有一种说法是 `throw` 用多了会影响性能，因为代码有可能跳到不可预知的位置。但是[结合 Promise 的实现](/web/js/runtime/promise/implement-then-method.md)，我们知道回调函数的执行都被包在了 `try-catch` 块里，所以代码的跳跃幅度是在可控范围之内的。因此，这里的性能损失可以忽略。

## 3. 组合工具

> composition tools

### 3.1 同时执行

[“同时”运行多个 promises](https://anjia1.gitbook.io/web/js/runtime/promise/pages/aTJDfEnE3L4mfauvZXOA#3.-jing-tai-fang-fa-zhi-bing-fa)，不相互阻塞。

* `Promise.race()`
* `Promise.all()`
* `Promise.allSettled()`
* `Promise.any()`

### 3.2 顺序执行

顺序执行多个 promises，比如：

```javascript
// eg1.
Promise.resolve()
    .then(func1)
    .then(func2)
    .then(func3)
    .then((result3) => {
        /* use result3 */
    })


// eg2. 用 async/await
let result;
for (const f of [func1, func2, func3]) {
    result = await f(result)
} // use last result (i.e. result3)


// eg3.
[func1, func2, func3]
    .reduce((p, f) => p.then(f), Promise.resolve())
    .then((result3) => {
        /* use result3 */
    })

    
// eg4. 可重用的 compose 函数
//      在 functional programming（函数式编程）中很常见
const applyAsync = (acc, val) => acc.then(val)
const composeAsync =
    (...funcs) =>
        (x) =>
            funcs.reduce(applyAsync, Promise.resolve(x))
```

在决定按“顺序”组合 promises 之前，需要考虑下是否真的有一个 promise 的执行依赖于另一个的结果。毕竟，相比“顺序”执行 promises，还是“同时”执行多个 promises 效率更高些。

## 4. 其它

### 4.1 解决回调地域

> callback pyramid of doom，厄运的回调金字塔

回调地域，可以用 promise 的链式调用来解决。原理是：

```js
function successCallback(result) {
  console.log(`Audio file ready at URL: ${result}`)
}

function failureCallback(error) {
  console.error(`Error generating audio file: ${error}`)
}

// 之前的写法，其中 audioSettings 函数里有异步操作 
createAudioFileAsync(audioSettings, successCallback, failureCallback)

// 有了 Promise 之后的写法，callbacks 被附加到了返回的 promise 对象上
createAudioFileAsync(audioSettings).then(successCallback, failureCallback)
```

举个例子：

```javascript
// 回调地域
doSomething((result) => {
    doSomethingElse(result, (newResult) => {
        doThirdThing(newResult, (finalResult) => {
            console.log(`Got the final result: ${finalResult}`)
        }, failureCallback)
    }, failureCallback)
}, failureCallback)

// 链式调用，清晰简洁明了
doSomething()
    .then((result) => doSomethingElse(result))
    .then((newResult) => doThirdThing(newResult))
    .then((finalResult) => {
        console.log(`Got the final result: ${finalResult}`)
    })
    .catch(failureCallback)
```
