iT邦幫忙

2024 iThome 鐵人賽

DAY 26
0
JavaScript

Vue.js學習中的細節陷阱:30天自我學習指南系列 第 26

Day 26: JavaScript 的錯誤處理和 Vue元件錯誤捕捉 - onErrorCaptured

  • 分享至 

  • xImage
  •  

昨天結束了5天的SOLID設計之旅,今天來回歸到 JavaScript 一些沒注意的觀念吧~~。

今天要聊的是錯誤處理,讓開發者能夠在程式發生錯誤時作出即時反應,避免應用程式崩潰,來確保系統穩定性和用戶體驗,些觀念應該多多少少都看過和用過,但有時反而容易忽略呢~今天來回顧一下。

今日學習目標

  1. JavaScript 的錯誤處理 try/catch/finally 語法
  2. 非同步流程的錯誤處理方式-有流程性的API如何處理
  3. Vue的元件錯誤捕捉器-onErrorCaptured

JavaScript 的錯誤處理 try/catch/finally 語法

在 JavaScript 中,錯誤其實是一種物件,當程式發生異常時會自動生成。這些錯誤物件包含有關錯誤的類型、引發錯誤的具體語句:

throw TypeError("Wrong type found, expected character")
  • throw: 用來顯式拋出一個錯誤,當這個錯誤被拋出時,會立即中斷當前的程式執行。

  • TypeError:JavaScript 中的一種內建錯誤類型,通常用來表示某種不匹配的數據類型。

錯誤堆疊 stack 追蹤

還記得我們Day 10: JavaScript事件循環、宏任務和微任務 ,複習非同步運作原理時,提到JavaScript程式代碼在同步運行時 Call Stack堆疊 嗎?當發生錯誤時,錯誤訊息一樣會跟函式執行一樣先進後出,錯誤也會從內層函式逐步向外層傳遞。

錯誤訊息首先列印出錯誤的名稱和描述,接著是呼叫方法的堆疊追蹤清單。每個方法呼叫都包含對應的原始程式碼位置和行號。可以利用這些資訊,逐步檢查程式碼,確定是哪段程式碼引發了錯誤,應該大多數程式錯誤滿常看到的畫面。

https://ithelp.ithome.com.tw/upload/images/20241009/20145251zyIDMcLJ8i.png

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)
}

如何防止 JavaScript 中的錯誤造成程式中斷

我們使用 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錯誤捕抓是同步的

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如何處理

在實務上非同步處理函式大多是跟後端API交互取得資料的時候,其中可以根據API之間的依賴性選擇不同的方式來執行。主要有兩種情況:

  • API之間沒有相互依賴關係
  • API 有先後順序,上一段結果作為下一段的參數

因為有時候一個儲存按鈕的動作,同一步驟通常會有 4~5 支請求從前端發出,在除錯過程中如何正確掌握哪個流程出錯呢?

API之間沒有相互依賴關係

當所有的 API 請求相互獨立,沒有依賴彼此的結果時,可以同時發送所有請求並同時等待它們完成,可以使用 Promise.allPromise.allSettled()

  • Promise.all

當所有請求都成功時,會返回所有結果;如果其中任何一個請求失敗,整個流程會中止並返回錯誤。

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',不知道哪裡有錯誤

  • Promise.allSettled

無論每個請求的結果是成功還是失敗,最終都會返回所有請求的結果。適合需要了解所有請求結果的情況。

callMutiApi 內改成 Promise.allSettled(promises),會發現會回傳所有非同步請求結果,並顯示各個請求成功或失敗狀態。

https://ithelp.ithome.com.tw/upload/images/20241009/20145251N9zg22zp9y.png

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 ()) // 

API 有先後順序,上一段結果作為下一段的參數

可以使用JavaScript的高階函式(HOF)-reduce,達到累加的效果。

  • 先定義好各支API的回傳值和錯誤訊息
  • reduce() 初始值為promise.resove(),並輸入初始值initialValue,讓第一支API函式能夠順利運作。
  • 每次reduce執行時,apiFunction(res) 使用 await 來確保它會等待上一次的請求完成,並接收上一次的結果 res 作為參數。
  • 因為我們有設定 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 的元件錯誤捕捉器-onErrorCaptured

對於 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語法捕捉樣板中的錯誤

  • 像是樣板中常見的JS表達式運算轉換,我們常常可以在樣板中對資料做處理,不過常常發現資料型別由文字轉成數字等,就發生調用錯誤等。
  • 因為某段元件無法正確描述資料型態,導致整個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>

試試製作一個錯誤邊界元件(Error boundary component)

其實 Vue3元件生命週期hook裡面,有開發一隻api -onErrorCaptured,專門捕捉元件內不預期產生的錯誤,並且類似設置斷點,將錯誤隔離在元件內而不影響整個Vue App跟著出錯故障。

可以利用 onErrorCaptured實作一個捕捉元件錯誤的容器組件,功能很簡單能夠捕捉到元件發生的錯誤,並且在錯誤發生時轉成一段錯誤提示訊息UI。

onErrorCaptured 會接收到3個參數:

  • 錯誤對象err
  • 錯誤組件的组件的實例物件vm
  • 包含錯誤信息的字符串info

錯誤訊息info會以官方對應的訊息告訴你是哪段runtime function 出線錯誤,已上面案例來說,因為是樣板編譯形成渲染函式過程有誤,會顯示是render function

  • onErrorCaptured 最終會一個回傳布林值,主要功能為發生訊息錯誤是否要將錯誤往上傳遞,通常我們做了元件錯誤捕捉會希望返回false,不要在向上傳遞
  • 如果組件的繼承鍊中有包含多個子組件,它們都用 onErrorCaptured 包住使用的話,對於同一元件錯誤,會一一由子組件向父組件向上傳遞。類似DOM原生的事件冒泡(event pronogation)那樣。
  • 最終所有錯誤都會收集到全域最上層的 app.config.errorHandler。
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>

總結

  • JavaScript 中的錯誤處理提供了 try/catch/finally 來捕捉和處理同步錯誤,避免程式碼運行突然中斷。
  • 在非同步操作 API 請求中,可以使用 Promise 結合 async/await 來捕捉錯誤,另外,善用Promise.allPromise.allSettled 可以同時處理多個 API 請求結果,或是做一些流程性上的錯誤訊息捕捉幫助除錯。
  • Vue 則提供 onErrorCaptured 來捕捉元件內的錯誤,將錯誤訊息力度,以元件為單位切了一刀,對於大型應用程式更能夠降低中斷執行的發生率。

學習資源

  1. https://medium.com/hannah-lin/錯誤處理-error-handling-in-js-4275bae1a2c4
  2. https://javascript.info/promise-error-handling
  3. https://javascriptinfo.dev.org.tw/try-catch
  4. https://medium.com/swlh/error-handling-in-javascript-a-quick-guide-54b954427e47
  5. https://kinsta.com/blog/errors-in-javascript/#syntaxerror
  6. https://medium.com/dean-lin/javascript-如何讓-await-函式並行-從實際案例了解-promise-all-和-promise-allsettled-的區別-bea062893091
  7. https://enterprisevue.dev/blog/error-handling-in-vue-3/

上一篇
Day 25: SOLID - 依賴反轉原則(DIP) 和 Vue 的依賴注入模式
下一篇
Day 27: JavaScript 模組(module) 和 Vue的程式碼分割 (code spliting)
系列文
Vue.js學習中的細節陷阱:30天自我學習指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言