JavaScript 之旅 (8):Promise.prototype.finally()

留言

目錄

  1. Promise Chain
  2. Promise.prototype.finally() 的回傳值永遠是 Promise
  3. Promise.prototype.finally() 的 callback
    1. Promise.prototype.finally() 的 callback 沒有 argument
    2. Promise.prototype.finally() 的 callback 會被忽略 return
  4. Promise.prototype.finally() vs. finally clause
    1. return 值
    2. throw 值
    3. 一定會執行的 finally
      1. finally clause 一定會在最後執行
      2. 不管是 fulfilled 或 rejected,Promise.prototype.finally() 的 callback 都會執行
  5. Promise.prototype.finally() vs. Promise.prototype.then(func, func)
  6. 情境範例
  7. 資料來源

本篇介紹 ES2018 (ES9) 提供的 Promise.prototype.finally()

本文同步發表於 iT 邦幫忙:JavaScript 之旅 (8):Promise.prototype.finally()

「JavaScript 之旅」系列文章發文於:

下面是幾個非同步處理很常見的情境:

  • 進入某頁面時,會立即發 AJAX request,在拿到 response 之前都會顯示「正在載入…」的訊息,不管是拿到 response,還是發生錯誤,都會隱藏「正在載入…」
  • 不管某個操作是否完成,都要紀錄 log
  • 建立 DB 連線來搜尋資料時,不管是成功拿到,還是中途出現錯誤,都要關閉連線釋放資源

以上情境都有一個共通點:不管做什麼事,最後都要做某件事。

也許你會想到 try-finally,會希望非同步處理的 Promise 上也有 finally 的功能 (我自己是沒想過啦 XD),這就是今天要介紹的 Promise.prototype.finally()

在過去原生的 Promise 沒有提供 finally 功能時,很多 library 都在非同步處理的 API 上實作了 finally() 方法,此方法是用來註冊一個在 promise settled 時 (即 fulfilled 或 rejected) invoke 用的 callback。

更多 library 的實作可參閱:

在 ES2018 (ES9) 提供了 Promise.prototype.finally() 新的 Promise method。當 promise settled 時 (即 fulfilled 或 rejected),會執行指定的 callback。

Promise Chain

先說明什麼是 promise chain,因為之後會常常看到這個專有名詞。

將多個 Promise 串在一起,以表達一個序列的非同步執行步驟,而這個序列就是 promise chain。

那為何是 chain?因為每次在 Promise 上呼叫 .then().catch().finally()Promise method 時,都會建立並回傳新的 Promise。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise.resolve('OK')
.then(result => {
console.log(result);
return Promise.resolve('Hi')
})
.then(result => {
console.log(result);
return Promise.reject('Oops');
})
.catch(error => {
console.log(error);
})
.finally(() => {
console.log('finally');
});

// OK
// Hi
// Oops
// finally

Promise.prototype.finally() 的回傳值永遠是 Promise

Promise.prototype.finally() 的回傳值永遠是 Promise 物件,該 promise 可能會 fulfilled 或 rejected,那何時會 fulfilled?還是會 rejected?

先講結論:要看你的 promise chain 是怎麼寫的

  • .finally() 的前一個 Promise 是 fulfilled,那 .finally() 回傳的 Promise 就會是 fulfilled
  • .finally() 的前一個 Promise 是 rejected,那 .finally() 回傳的 Promise 就會是 rejected

先看幾個範例:

假設我先執行 Promise.resolve('OK'),該 promise 會立即 fulfilled,將 OK 傳給 .then() 的 callback,所以第一個輸出訊息會是 OK,接著執行 .finally(),並將 .finally() 的回傳值存在一個名為 promiseA 的變數:

1
2
3
4
5
6
7
8
9
10
let promiseA = Promise.resolve('OK')
.then(result => {
console.log(result);
})
.finally(() => {
console.log('finally');
});

// OK
// finally

接著印出 promiseA,它是一個 Promise 物件,該 promise 已經 fulfilled 了,且 fulfilled 的值為 undefined

1
2
console.log(promiseA);
// Promise {<fulfilled>: undefined}

那為何 fulfilled 的值會是 undefined,因為在 promise chain 中,.finally() 的前一個 Promise 是 .then() 回傳的,而 .then() 的 callback 沒有回傳值,所以才會是 undefined

所以不要搞錯了,promiseA 存的不是 Promise.resolve('OK') 回傳的 Promise 物件,而是最後一個 promise chain 的。

那再看下一個範例,這次拿到 .then() 這個步驟,一樣將的回傳值存起來,存在一個名為 promiseB 的變數:

1
2
3
4
5
6
let promiseB = Promise.resolve('OK')
.finally(() => {
console.log('finally');
});

// finally

接著印出 promiseB,該 promise 一樣已經 fulfilled 了,但這次 fulfilled 的值是 OK

1
2
console.log(promiseB);
// Promise {<fulfilled>: "OK"}

為什麼會是 OK?因為在 promise chain 中,.finally() 的前一個 Promise 是 Promise.resolve('OK') 回傳的,該 Promise fulfilled 的值就是 OK,所以才會是 OK

所以就如同前面結論說的,.finally() 回傳的 Promise 是 fulfilled 還是 rejected,是依據 promise chain 中前一個 Promise 來決定的。

Promise.prototype.finally() 的 callback

Promise.prototype.finally() 的 callback 沒有 argument

.then().catch() 的 callback 會有 argument,而該 argument 是在 promise chain 中,前一個 Promise 的 fulfilled 值或 rejected 值。

Promise.prototype.finally() 的 callback 是沒有 argument 的,若你還是寫了 argument,其值也會是 undefined,不管 promise chain 中的前一個 Promise 的 fulfilled 或 rejected:

1
2
3
4
5
6
Promise.resolve('OK')
.finally(value => {
console.log(value);
});

// undefined
1
2
3
4
5
6
Promise.reject('Oops')
.finally(value => {
console.log(value);
});

// undefined

Promise.prototype.finally() 的 callback 會被忽略 return

Promise.prototype.finally() 的 callback 中的 return 會被忽略,但回傳的 Promise 的 fulfilled 值或 rejected 值會是 promise chain 中,前一個 Promise 的 fulfilled 值或 rejected 值。:

例如:Promise.resolve('OK') 會立即 fulfilled,接著在 .finally()return 會被忽略,但 .finally() 回傳的 Promise 的 fulfilled 值會跟 Promise.resolve('OK') 回傳的 fulfilled 值相同。

1
2
3
4
5
6
7
8
Promise.resolve('OK')
.finally(() => {
console.log('finally...');
return 'finally';
})
.then(value => {
console.log(value);
});

若拆開 promise chain 就會更容易看出來:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let promiseA = Promise.resolve('OK');
console.log(promiseA);
// Promise {<fulfilled>: "OK"}


let promiseB = promiseA.finally(() => {
console.log('finally...');
return 'finally';
});
// finally...

console.log(promiseB);
// Promise {<fulfilled>: "OK"}


let promiseC = promiseB.then(value => {
console.log(value);
});
// OK

console.log(promiseC);
// Promise {<fulfilled>: "undefined"}

promise rejected 的情況也一樣,你可以試著將上面的 Promise.resolve('OK') 改成 Promise.reject('Oops') 觀察看看。

Promise.prototype.finally() vs. finally clause

先來看兩者的寫法。

下面是 Promise.prototype.finally() 的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
Promise.resolve('OK')
.then(result => {
console.log(result);
})
.catch(error => {
console.log('error');
})
.finally(() => {
console.log('finally');
});

// OK
// finally

而下面是 try 陳述句中 finally clause 的用法:

1
2
3
4
5
6
7
8
9
10
try {
console.log('OK');
} catch (error) {
console.log('error');
} finally {
console.log('finally');
}

// OK
// finally

兩者有些地方很相識,但用法和行為都不同,下面會提出它們的不同之處。

return

Promise.prototype.finally() 會回傳 Promise,該 Promise 可能會 fulfilled 或 rejected (前面有說明)。

finally 只是 try 陳述句中的 clause,若在 finally clause 內 return 某個值會成為 function 的回傳值。

例如:在 func() 函數中,finally clause 內 returnfunc 就成為此函數的回傳值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function func() {
try {
console.log('try');
} catch (error) {
console.log('catch');
} finally {
console.log('finally');
return 'func';
}
}

let result = func();
// try
// finally

console.log(result);
// "func"

throw

若在 finally clause 內使用 throw,需要讓另一個 try-catch 來捕捉錯誤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function func() {
try {
console.log('try');
} finally {
console.log('finally');
throw new Error('Oops');
}
}

try {
func();
} catch(error) {
console.log(error);
}

// try
// finally
// Error: Oops
// at func (<anonymous>:6:11)
// at <anonymous>:11:3

而在 Promise.prototype.finally() 的 callback 中使用 throw,會讓回傳的 Promise rejected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let promiseA = Promise.resolve('OK')
.then(result => {
console.log(result);
});

// OK

console.log(promiseA);
// Promise {<fulfilled>: undefined}


let promiseB = promiseA.finally(() => {
console.log('finally');
throw new Error('Oops');
});

// finally
// Uncaught (in promise) Error: Oops
// at <anonymous>:3:11
// at <anonymous>

console.log(promiseB);
// Promise {<rejected>: Error: Oops
// at <anonymous>:3:11
// at <anonymous>}

一定會執行的 finally

Promise.prototype.finally()finally clause 的其中一個共通點就是一定會執行。

finally clause 一定會在最後執行

先來說明 finally clause。

在函數內的 try clause 或 catch clause 裡面 return 某個值,函數會在回傳該值之前,先執行 finally clause 內的程式碼 (所以 finally 就如其名,真的是「最後」)。

例如:在 try clause 內 return 值,不會在回傳後直接結束此函數的執行,而是會在回傳之前先執行 finally clause 內的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function func() {
try {
console.log('try');
return 'func';
} catch (error) {
console.log('catch');
} finally {
console.log('finally');
}
}

console.log(func());
// try
// finally
// "func"

另一個範例:在 catch clause 內 return 值,不會在回傳後直接結束此函數的執行,而是會在回傳之前先執行 finally clause 內的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function func() {
try {
console.log(data);
} catch (error) {
console.log('catch');
return 'func';
} finally {
console.log('finally');
}
}

console.log(func());
// catch
// finally
// "func"

不管是 fulfilled 或 rejected,Promise.prototype.finally() 的 callback 都會執行

不管 Promise 是 fulfilled 或 rejected 都會執行 Promise.prototype.finally() 的 callback。

例如:Promise.resolve('OK') 會回傳的 promise 立即 fulfilled 後,會執行 .finally() 的 callback:

1
2
3
4
5
6
7
8
9
let promiseA = Promise.resolve('OK')
.finally(() => {
console.log('finally');
});

// finally

console.log(promiseA);
// Promise {<fulfilled>: "OK"}

另一個例子:Promise.reject('Oops') 會回傳的 promise 立即 rejected 後,會執行 .finally() 的 callback:

1
2
3
4
5
6
7
let promiseB = Promise.reject('Oops')
.finally(() => {
console.log('finally');
});

console.log(promiseB);
// finally

Promise.prototype.finally() vs. Promise.prototype.then(func, func)

.finally().then(onFinally, onFinally) 很像,但有些差別。

因為沒有可靠的方法來確定 promise 是 fulfilled 或 rejected,所以 .finally() 的 callback 不會接收到任何 argument。代表這適用於不管是 fulfilled 還是 rejected 的情況。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Promise.resolve(2)
.then(() => {}, () => {});
// Promise {<resolved>: undefined}

Promise.resolve(2)
.finally(() => {});
// Promise {<resolved>: 2}

Promise.reject(3)
.then(() => {}, () => {});
// Promise {<resolved>: undefined}

Promise.reject(3)
.finally(() => {});
// Promise {<rejected>: 3}

.finally() 可避免在 then()catch() handler 中有重複的程式碼,即在建立 inline 函數時,可以只傳一次,避免重複宣告或為它宣告變數。

情境範例

前面提到一些情境,就拿其中一個作為範例。

假設進入某頁面時,會立即發 AJAX request,在拿到 response 之前都會顯示「正在載入…」的訊息,不管是拿到 response,還是發生錯誤,都會隱藏「正在載入…」。

範例程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let isLoading = true;
let JSON_API = 'https://jsonplaceholder.typicode.com/posts/1';
let HTML_API = 'https://developer.mozilla.org/en-US/docs/Web';

function fetchData(url) {
return fetch(url)
.then(response => {
console.log('isLoading:', isLoading);

const contentType = response.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
return response.json();
} else {
throw new TypeError(`Oops, we haven't got JSON!`);
}
})
.then(json => {
console.log('Success');
return json;
})
.catch(error => {
console.log(error);
})
.finally(() => {
isLoading = false;
console.log('isLoading:', isLoading);
});
}

// 試著換成 fetchData(HTML_API)
fetchData(JSON_API).then(data => {
console.log(data);
});

之後會提到 ?. Optional Chaining 運算子。

若 API 的 Content-Typeapplication/json (即 fetch(JSON_API) 這個 AJAX response),promise 就會 fulfilled 列印出 API 資料,並且在 finally 時將 isLoading 設為 false,所以輸出如下:

1
2
3
4
5
6
7
8
fetchData(JSON_API).then(data => {
console.log(data);
});

// isLoading: true
// Success
// isLoading: false
// {userId: 1, id: 1, title: "..."}

若 API 的 Content-Type 不是 application/json (即 fetch(HTML_API) 這個 AJAX response),promise 就會 rejected 列印出錯誤訊息,並且在 finally 時將 isLoading 設為 false,所以輸出如下:

1
2
3
4
5
6
7
8
fetchData(HTML_API).then(data => {
console.log(data);
});

// isLoading: true
// TypeError: Oops, we haven't got JSON!
// isLoading: false
// undefined

因為不管是 .then().catch() 都要執行 isLoading = false,那更好的作法就是統一在 .finally() 執行 isLoading = false,這樣就不用寫重複的邏輯了。

若上面的範例改用 async / await 的寫法也許會像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let isLoading = true;
let JSON_API = 'https://jsonplaceholder.typicode.com/posts/1';
let HTML_API = 'https://developer.mozilla.org/en-US/docs/Web';

async function fetchData(url) {
try {
const response = await fetch(url);
console.log('isLoading:', isLoading);

const contentType = response.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
console.log('Success');
return response.json();
} else {
throw new TypeError(`Oops, we haven't got JSON!`);
}
} catch(error) {
console.log(error);
} finally {
isLoading = false;
console.log('isLoading:', isLoading);
}
}

資料來源

分享:

討論區