接下來我們要聊一個非常非常重要的東西,也就是關於錯誤的處理,而這個處理又稱之為例外處理。
首先先聊聊例外處理是什麼,通常我們在開發系統的時候不免會遇到一些錯誤狀況,例如:使用者網路不穩定、使用者輸入錯誤的資料、忘記宣告變數、輸入到一半颱風來吹斷電線桿停電或者是上網到一半被隔壁屁孩射斷網路線這種諸如此類的狀況(之前真的有朋友跟我說他的網路線被屁孩射斷),因此如果沒有做好一些錯誤機制的防範的話,那麼系統在上線時是很容易降低客戶對於該系統的信任度,所以錯誤的處理上是非常重要的。
那麼為了避免這種狀況我們通常就會使用例外處理的方式處理,如果已經有開始撰寫 ES7 語法的人,相信非常的熟悉 async
、await
語法,想當然你也一定會使用 try...catch
語法,下面簡單寫一小段:
async function getData () {
try{
const res = await axios.get('xxxx');
} catch (error) {
console.error(error);
}
}
上面的程式碼簡單來講,就是當 AJAX 因意外失敗或者網路中斷時,就會自動改跑 catch
區塊,但是這邊要注意如果你是用於前端畫面給使用者看的話,請不要使用 console.log
當作錯誤提示,因為普通的使用者根本不知道什麼是網頁開發者工具(devToole),因此這邊只是一個範例程式碼而已。
而上方是一個簡單的例外錯誤處理,接下來我們將會進入 Python 環節了解 Python 的例外處理是如何撰寫的唷!
一開始先我們刻意製造一個錯誤狀況:
def sayHello():
print('Hello')
print(myName)
sayHello()
上面的程式碼看起來很正常對吧?但實際上執行是會出現「NameError: name 'myName' is not defined
」的錯誤唷~
oh!如果你問我為什麼會出現這個錯的話,罰你回去「從 JavaScript 角度學 Python(4) - 型別與變數」章節重看,因為這章節我有解釋唷~
那麼如果你這邊有使用例外處理的話,就可以避免程式碼因為錯誤而導致中斷,而讓後面其他的程式碼可以正常運作:
def sayHello():
print('Hello')
try:
print(myName)
except:
print('你忘記宣告變數了。')
sayHello()
除此之外你還可以針對錯誤類型來給予例外處理,什麼意思呢?舉例來講剛剛的範例程式碼會出現「NameError: name 'myName' is not defined
」的錯誤訊息,而這個錯誤類型是 NameError
,所以你可以特別針對 NameError
客製化例外處理:
def sayHello():
print('Hello')
try:
print(myName)
except NameError:
print('你忘記宣告變數了。')
except:
print('單純只是一個錯誤。')
sayHello()
但是錯誤類型的例外處理非常非常多種,所以我建議可以閱讀這一篇了解還有哪些常見的錯誤類型。
那 JavaScript 有沒有類似這種針對錯誤類型的例外處理呢?
基本上有,只是寫法上比較不一樣,JavaScript 是必須使用 instanceof
語法來判斷錯誤類型,舉例來講,當你語法沒有宣告就存取的話是會出現 ReferenceError
錯誤,因此寫法如下:
try {
console.log(Ray); // 沒有宣告的變數,會出現 「Uncaught ReferenceError: Ray is not defined」
} catch(error) {
if(error instanceof ReferenceError) {
console.log('你存取一個不存在的變數唷');
}
}
console.log('程式碼依然正常運作');
那麼透過上述兩種程式碼來講,你比較喜歡哪一種呢~
還有一種處理是 finally
,通常這個最常見於跟遠端伺服器要資料之後要做特定的事情,舉例來講我們在撰寫 Vue 的時候,通常會使用 Loading 套件來解決在跟遠端伺服器 AJAX 過程的等待時間,進而盡可能的提升使用者體驗,這邊我就舉例一個很常見不好寫法:
const app = {
data() {
return {
isLoading: false,
};
},
methods:{
async getData() {
try {
this.isLoading = true;
await axios.get('xxxxx');
this.isLoading = false;
} catch (error) {
console.log(error)
}
},
},
};
Vue.createApp(app).mount('#app');
上面寫法看似不好的原因在於,當 AJAX 若失敗的話,就會進入到例外處理 catch
,但是關閉 isLoading
的方式卻是在 try
中,因此這個 Loading 狀態就永遠不會關閉,有些人會乾脆 try
與 catch
都撰寫 this.isLoading = false;
就像下方一樣:
const app = {
data() {
return {
isLoading: false,
};
},
methods:{
async getData() {
try {
this.isLoading = true;
await axios.get('xxxxx');
this.isLoading = false;
} catch (error) {
console.log(error)
this.isLoading = false;
}
},
},
};
Vue.createApp(app).mount('#app');
但是你有想過嗎?今天只是一個 this.isLoading = false;
可能還感覺不出什麼,但若程式碼邏輯較複雜、較多的話,難道你兩處都要寫嗎?
所以這邊就要來介紹 finally
,只要你使用 finally
的話,那麼就只會需要寫一次即可:
const app = {
data() {
return {
isLoading: false,
};
},
methods:{
async getData() {
try {
this.isLoading = true;
await axios.get('xxxxx');
} catch (error) {
console.log(error)
} finally {
this.isLoading = false;
}
},
},
};
Vue.createApp(app).mount('#app');
因為不管成功與否 finally
必定就是會執行,這樣子也可以確保如果請求失敗時,也可以正常的關閉 Loading
狀態。
當然 Python 也是一樣的做法,不管怎麼樣如果你有一個必定要執行的動作,例如:關閉檔案或者是輸出某些訊息,那就可以使用 finally
來解決:
def sayHello():
print('Hello')
try:
print(myName)
except NameError:
print('你忘記宣告變數了。')
except:
print('單純只是一個錯誤。')
finally:
print('這是一定會執行的行為。')
sayHello()
所以你也可以把 finally
理解成「必定會執行」的區塊,這樣你學會了嗎?
聰明的你或許馬上就會聯想到 JavaScript 的 throw
,有一種狀況是比較特別,舉例來講我們在做 AJAX 新增、修改與刪除等操作時,後端可能不會吐相對應的 HTTP Code,有可能是吐給你 status
來判斷這一次新增資料的結果是 true
or false
,所以這時候我們就會使用 throw
將當前的狀態踢出到例外處理。
那為什麼會有這種狀況呢?假使後端不管成功與否都是吐給你 200 狀態碼,那 AJAX 判斷上當然會認為你是請求成功的,並不屬於失敗,所以就不會走 catch
的部分,以 JavaScript 角度來講,我們可能就會這樣寫:
const app = {
data() {
return {
isLoading: false,
};
},
methods:{
async getData() {
try {
this.isLoading = true;
const res = await axios.get('xxxxx');
if(!res.data.status) {
throw Error('錯誤');
}
} catch (error) {
console.log(error)
} finally {
this.isLoading = false;
}
},
},
};
Vue.createApp(app).mount('#app');
透過上面範例我們可以自己決定何時要拋出,但是在 Python 中就沒有 throw
的語法,取而代之的是使用 raise
語法,raise
非常好玩,如果你不加上要噴什麼樣的錯誤的話,預設會是拋出 RuntimeError
,你可以試著單獨執行 raise
看看:
raise # RuntimeError: No active exception to reraise
因此透過這個預設的特性就可以使用前面所學的技巧設置一個例外錯誤訊息:
status = True
try:
if(status):
raise
except RuntimeError:
print('Error', RuntimeError)
except:
print('單純只是一個錯誤。')
finally:
print('這是一定會執行的行為。')
如果以上錯誤的處理你還不滿意的話,你也可以自己建立一個 class 來自定屬於自己的例外,而這一點建議可以直接參考官方文件。
那麼今天關於例外處理的介紹也就到這邊告一個段落哩。
完蛋了,今天的「作者的話」我完全不知道該打什麼,想分享一下醉雞的製作過程,但是我發現前面我已經分享過了,後來思考後想再分享小瓦機器人被我捏爆的事情,結果我昨天也講過了,這一篇只好空下「作者的話」了。
不太理解這一段,為什麼是這麼解釋的:
def sayHello():
print('Hello')
print(myName)
sayHello()
「如果你是用 JavaScript 的角度去看的話確實是正常的」
另外想請教一點,由於剛開始學例外處理,鮮少用到finally,有點好奇一件事,如果以下兩種寫法執行內容相同的話,那finally的優勢是什麼?
async getData() {
try {
this.isLoading = true;
await axios.get('xxxxx');
} catch (error) {
console.log(error)
} finally {
this.isLoading = false;
}
}
async getData() {
this.isLoading = true;
try {
await axios.get('xxxxx');
} catch (error) {
console.log(error)
}
this.isLoading = false;
}
關於第一點在的部分其實我有調整過範例,結果下面那一段「如果你是用 JavaScript 的角度去看的話確實是正常的」忘記移除,感謝勘誤。
而第二點的部分我覺得問得滿好的,確實因為 async
與 await
特性關係而被堵塞,所以不論是放在 finally
之內或者是不放在 finally
之內確實感覺是沒有差異的。
但是如果今天是採用 then
的寫法就會有明顯差異。
可是習慣撰寫 finally
我個人認為是會比較好的,因為不論 try
或是 catch
被執行,我們都可以確保知道 finally
的內容必定會被執行而這對於將來要維護的工程師來講,可以透過閱讀程式碼就知道 finally
會在 try
與 catch
執行後必定會執行一次,若不寫在這裡面的話,可能有些人不清楚就會需要 try try 看才知道哩。
所以實際上我也有看過不寫 finally
的哩