昨天結束了5天的SOLID設計之旅,今天來回歸到 JavaScript 一些沒注意的觀念吧~~。
今天要聊的是錯誤處理,讓開發者能夠在程式發生錯誤時作出即時反應,避免應用程式崩潰,來確保系統穩定性和用戶體驗,些觀念應該多多少少都看過和用過,但有時反而容易忽略呢~今天來回顧一下。
try/catch/finally 語法
onErrorCaptured
在 JavaScript 中,錯誤其實是一種物件,當程式發生異常時會自動生成。這些錯誤物件包含有關錯誤的類型、引發錯誤的具體語句:
throw TypeError("Wrong type found, expected character")
throw: 用來顯式拋出一個錯誤,當這個錯誤被拋出時,會立即中斷當前的程式執行。
TypeError:JavaScript 中的一種內建錯誤類型,通常用來表示某種不匹配的數據類型。
還記得我們Day 10: JavaScript事件循環、宏任務和微任務 ,複習非同步運作原理時,提到JavaScript程式代碼在同步運行時 Call Stack堆疊
嗎?當發生錯誤時,錯誤訊息一樣會跟函式執行一樣先進後出,錯誤也會從內層函式逐步向外層傳遞。
錯誤訊息首先列印出錯誤的名稱和描述,接著是呼叫方法的堆疊追蹤清單。每個方法呼叫都包含對應的原始程式碼位置和行號。可以利用這些資訊,逐步檢查程式碼,確定是哪段程式碼引發了錯誤,應該大多數程式錯誤滿常看到的畫面。
function functionA() {
functionB()
}
function functionB() {
functionC()
}
function functionC() {
throw new Error("Something went wrong!")
}
try {
functionA()
} catch (error) {
console.error("錯誤:", error.message)
console.error("堆疊追蹤:", error.stack)
}
我們使用 try/catch
語法來收集錯誤,這樣可以避免程式有錯誤遭到中斷執行,try 區塊中的程式碼是同步執行的
。一旦在 try 區塊中發生錯誤並拋出錯誤物件
,程式會立刻跳到 catch
區塊避免程式中斷。
在 catch
區塊中,我們可以根據錯誤的類型(例如 TypeError 或 RangeError)進行不同的客製化訊息等處理。
如果我們在 catch
區塊中不希望程式終止,不需要再次 throw 錯誤,否則這會再次拋出錯誤並中斷程式的執行流程
,除非在外圍的區塊(outer socope)有再次使用try/catch
捕捉。
try {
// business logic code
} catch (exception) {
if (exception instanceof TypeError) {
// do something
} else if (exception instanceof RangeError) {
// do something else
}
} fianlly {
// 最終一定會走到這段
console.log("final")
}
try/catch
語法可以再接著 finally
,finally內的程式碼是不管有沒有捕捉的錯誤結果都會執行代碼,但如果在try區塊中已經有返回值(return)出現,一樣會先執行完finally內程式碼再返回回傳值,算是小小沒發現的細節
。
function func() {
try {
return 1;
} catch (err) {
/* ... */
} finally {
alert( 'finally' );
}
}
alert( func() ); // 還是會看到alert彈窗的執行
try/catch
語法本身只能補捉到同步階段的錯誤,setTimeout 是一個非同步操作
,try/catch 無法捕捉到在 setTimeout
中引發的錯誤。
const calculateCube = (number, callback) => {
setTimeout(() => {
if (typeof number !== "number")
throw new Error("Numeric argument is expected")
const cube = number * number * number
callback(cube)
}, 1000)
}
const callback = result => console.log(result)
try {
calculateCube(4, callback)
} catch (e)
{
console.log(e)
}
哪麼面對這種常見狀況該如何處理:
利用 async/await
語法,並用 Promise
物件將 setTimeout
非同步邏輯封裝起來,並定義明確的錯誤處理: 因為 await 本身會解析 Promise 的狀態,若 Promise 被 reject拒絕,會自動將程式跳轉到 catch 區塊執行。
const calculateCube = async (number) => {
try {
const result = await new Promise((resolve, reject) => {
setTimeout(() => {
if (typeof number !== "number") {
reject(new Error("Numeric argument is expected"));
} else {
resolve(number * number * number);
}
}, 1000);
});
console.log(result);
} catch (err) {
console.error(err.message); // 捕捉異步錯誤
}
};
calculateCube(4); // 正確情況
calculateCube("not a number"); // 捕捉錯誤
在實務上非同步處理函式大多是跟後端API交互取得資料的時候,其中可以根據API之間的依賴性選擇不同的方式來執行。主要有兩種情況:
因為有時候一個儲存按鈕的動作,同一步驟通常會有 4~5 支請求從前端發出,在除錯過程中如何正確掌握哪個流程出錯呢?
當所有的 API 請求相互獨立,沒有依賴彼此的結果時,可以同時發送所有請求並同時等待它們完成,可以使用 Promise.all
或 Promise.allSettled()
。
當所有請求都成功時,會返回所有結果;如果其中任何一個請求失敗,整個流程會中止並返回錯誤。
const delay = delay => new Promise(resolve => setTimeout(resolve, delay));
async function api1 () {
await delay(500);
return "A";
}
async function api2 () {
await delay(500);
return "B";
}
async function api3 () {
await delay(500);
throw "error"; // 丟出錯誤訊息
}
async function callMutiApi () {
const promises = [
api1(),
api2(),
api3(),
];
Promise.all(promises)
.then(values => console.log(values))
.catch(err => console.log(err))
.finally(() => console.timeEnd())
}
console.log('callMutiApi',callMutiApi ()) // 只會打印'error',不知道哪裡有錯誤
無論每個請求的結果是成功還是失敗,最終都會返回所有請求的結果。適合需要了解所有請求結果的情況。
將 callMutiApi
內改成 Promise.allSettled(promises)
,會發現會回傳所有非同步請求結果,並顯示各個請求成功或失敗狀態。
async function callMutiApi () {
const promises = [
api1(),
api2(),
api3(),
];
Promise.allSettled(promises)
.then(values => console.log(values))
.catch(err => console.log(err))
.finally(() => console.timeEnd())
}
console.log('callMutiApi',callMutiApi ()) //
可以使用JavaScript的高階函式(HOF)-reduce,達到累加的效果。
初始值initialValue
,讓第一支API函式能夠順利運作。因為我們有設定 response.message 是 'success',才能回傳 response,並傳給下一個API使用,否則失敗沒有參數拋出錯誤,並中斷整個API鍊的過程
。// api A
async function getApiA(params) {
try {
const res = await axios.get('urlA',{params: params})
return {
message: 'sucess',
data: res
}
} catch (err) {
console.warn(err,'API-A有誤')
return {
message: 'fail',
data: []
}
}
}
async function getApiB(params) {
try {
const res = await axios.get('urlB',{params: params})
return {
message: 'sucess',
data: res
}
} catch (err) {
console.warn(err,'API-B有誤')
return {
message: 'fail',
data: []
}
}
}
const apiFunctions = [getApiA, getApiB];
async function executeRequests(apiFunctions,initialParams) {
await apiFunctions.reduce(async (promiseChain, apiFunction) => {
const res = await promiseChain; // 確保上一個請求完成後才執行下一個
try {
const response = await apiFunction(res); // 執行 API 函數,帶入上一次api回傳結果作為參數
if (response.message === 'sucess') {
console.log('API 請求成功:', response.data);
return response; // 回傳給下一次reduce
} else {
console.warn('API 請求失敗:', response);
throw new Error('API 請求失敗,流程中斷');
}
} catch (error) {
console.error('API 請求執行錯誤:', error);
throw error; // 中斷鏈條並傳播錯誤
}
}, Promise.resolve(initialParams)); // 從 resolved 的 Promise 開始,並輸入第一支API需要的參數
}
executeRequests(apiFunctions).catch(error => {
console.error('執行過程中斷:', error);
});
對於 Vue 開發初心者來說,利用上面學過的 try/catch
語法,不能夠有效處理Vue元件的錯誤嗎?
對於某些重要的函式像是獲取api資料等重要功能,我們可能以try/catch
包裹。不過一個Vue組件裡除了 <script> 資料更新
、事件
和wacth
等邏輯外,還包含了<template>
。
在Day 2: Vue SFC樣板(Template)和渲染函式(Render Function),有提過 Vue 的SFC文件檔中的 <template>
,但其實會透過 @vue-sfc-complier
先解析出來,最終呼叫 createElement
產生一個帶有描述特徵的JS物件。這段complier過程中的確有可能出現錯誤,沒辦法用try/catch語法捕捉樣板中的錯誤
。
資料型別由文字轉成數字等,就發生調用錯誤
等。因為某段元件無法正確描述資料型態,導致整個component tree有錯誤無法形成,就造成整個App就白頁了
。如果以自己本身碰過的例子,像是v-for 列表渲染、或者接收api資料的元件會比較容易出錯,因為可能api資料型別變換,或者空值沒定義好回傳null造成不可預期錯誤。
<template>
<div class="greetings">
<h1 class="green">{{ msg.split('')[0] }}</h1>
<h3>
You’ve successfully created a project with
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<script setup>
defineProps({
msg: {
type: String,
}
})
</script>
其實 Vue3元件生命週期hook裡面,有開發一隻api -onErrorCaptured
,專門捕捉元件內不預期產生的錯誤,並且類似設置斷點,將錯誤隔離在元件內而不影響整個Vue App跟著出錯故障。
可以利用 onErrorCaptured
實作一個捕捉元件錯誤的容器組件,功能很簡單能夠捕捉到元件發生的錯誤,並且在錯誤發生時轉成一段錯誤提示訊息UI。
onErrorCaptured
會接收到3個參數:
錯誤訊息info會以官方對應的訊息告訴你是哪段runtime function 出線錯誤,已上面案例來說,因為是樣板編譯形成渲染函式過程有誤,會顯示是render function
。
onErrorCaptured
最終會一個回傳布林值
,主要功能為發生訊息錯誤是否要將錯誤往上傳遞,通常我們做了元件錯誤捕捉會希望返回false,不要在向上傳遞
。onErrorCaptured
包住使用的話,對於同一元件錯誤,會一一由子組件向父組件向上傳遞
。類似DOM原生的事件冒泡(event pronogation)那樣。app.config.errorHandler = (err, instance, info) => {
// handle error, e.g. report to a service
}
如果一個元件比較龐大或真的有比較容易故障的大型元件,應該可以試試:
<template>
<div v-if="isError">{{ errorMessage }}</div>
<div v-else>
<slot></slot>
</div>
</template>
<script setup>
import {onErrorCaptured,ref} from 'vue'
const isError = ref(false)
const errorMessage = ref("")
onErrorCaptured((err, vm, info)=>{
console.error('[捕獲錯誤]', err.message,'vm',vm,'info',info)
isError.value = true
errorMessage.value = 'something wrong'
return false // 這段滿重要的----------------------
})
</script>
// 在其他元件引入使用
<div class="wrapper">
<ErrorBoundary>
<HelloWorld :msg=123456 />
</ErrorBoundary>
<ErrorBoundary>
<ButtonClick></ButtonClick>
</ErrorBoundary>
</div>
try/catch/finally
來捕捉和處理同步錯誤,避免程式碼運行突然中斷。Promise 結合 async/await 來捕捉錯誤
,另外,善用Promise.all
和 Promise.allSettled
可以同時處理多個 API 請求結果,或是做一些流程性上的錯誤訊息捕捉幫助除錯。onErrorCaptured
來捕捉元件內的錯誤,將錯誤訊息力度,以元件為單位切了一刀,對於大型應用程式更能夠降低中斷執行的發生率。