函數(shù)式 try-catch 如何轉變 JavaScript 代碼
這種情況有多常見?
function writeTransactionsToFile(transactions) {
let writeStatus;
try {
fs.writeFileSync('transactions.txt', transactions);
writeStatus = 'success';
} catch (error) {
writeStatus = 'error';
}
// do something with writeStatus...
}
這是另一個我們想要一個取決于是否存在異常的值的實例。
通常, 我們可能會在 try-catch 的范圍之外創(chuàng)建一個可變變量,以便在其中和之后無錯誤地訪問。
但情況并非總是這樣。只要有一個函數(shù)式的 try-catch 就不會這樣。
一個純粹的 tryCatch() 函數(shù)避免了可變變量,并在我們的代碼庫中鼓勵可維護性和可預測性。
沒有修改外部狀態(tài) - tryCatch() 封裝了整個錯誤處理邏輯并產生單一輸出。
我們的 catch 變成了一個不需要大括號的單行代碼:
function writeTransactionsToFile(transactions) {
// 我們現(xiàn)在可以使用 const 了
const writeStatus = tryCatch({
tryFn: () => {
fs.writeFileSync('transactions.txt', transactions);
return 'success';
},
catchFn: (error) => 'error'
});
// do something with writeStatus...
}
tryCatch() 函數(shù)
那么,這個 tryCatch() 函數(shù)究竟是什么樣子的呢?
從我們以上的使用方式,你已經(jīng)可以猜到定義了:
function tryCatch({ tryFn, catchFn }) {
try {
return tryFn();
} catch (error) {
return catchFn(error);
}
}
為了正確地講述函數(shù)的作用,我們確保使用對象參數(shù)來明確參數(shù)名稱——即使只有兩個屬性。
因為編程不僅僅是達到目的的手段 - 我們還在講述從開始到結束的代碼庫中的對象和數(shù)據(jù)的故事。
TypeScript 在這樣的情況下非常好用;我們看看一個泛型類型的 tryCatch() 可能是什么樣子:
type TryCatchProps<T> = {
tryFn: () => T;
catchFn: (error: any) => T;
};
function tryCatch<T>({ tryFn, catchFn }: TryCatchProps<T>): T {
try {
return tryFn();
} catch (error) {
return catchFn(error);
}
}
我們用 TypeScript 重寫功能性 writeTransactionsToFile() :
function writeTransactionsToFile(transactions: string) {
// 返回 'success' 或 'error'
const writeStatus = tryCatch<'success' | 'error'>({
tryFn: () => {
fs.writeFileSync('transaction.txt', transactions);
return 'success';
},
catchFn: (error) => return 'error';
});
// do something with writeStatus...
}
我們使用 'success' | 'error' 聯(lián)合類型來限制我們可以從 try 和 catch 回調中返回的字符串。
異步處理
不,我們完全不需要擔心這個問題 - 如果 tryFn 或 catchFn 是 async ,那么 writeTransactionToFile() 會自動返回一個 Promise 。
這是我們大多數(shù)人應該熟悉的另一個 try-catch 情況:發(fā)出網(wǎng)絡請求并處理錯誤。
在這里,我們根據(jù)請求是否成功來設置一個外部變量(在try-catch 之外)——在 React 應用中,我們可以輕松地用它設置狀態(tài)。
顯然,在真實世界的應用程序中,請求將會是異步的,以避免阻塞用戶界面線程:
async function comment(comment: string) {
type Status = 'error' | 'success';
let commentStatus: Status;
try {
const response = await fetch('https://api.mywebsite.com/comments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ comment }),
});
if (!response.ok) {
commentStatus = 'error';
} else {
commentStatus = 'success';
}
} catch (error) {
commentStatus = 'error';
}
// do something with commentStatus...
}
我們再次需要在這里創(chuàng)建一個可變變量,以便它可以進入 try-catch 并且沒有作用域錯誤地成功出來。
我們像以前一樣進行重構,這次,我們 async 了 try 和 catch 函數(shù),從而 await 了 tryCatch() :
async function comment(comment: string) {
type Status = 'error' | 'success';
// ?? await because this returns Promise<Status>
const commentStatus = await tryCatch<Status>({
tryFn: async () => {
const response = await fetch('https://api.mywebsite.com/comments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ comment }),
});
// ?? functional conditional
return response.ok ? 'success' : 'error';
},
catchFn: async (error) => 'error',
});
// do something with commentStatus...
}
可讀性,模塊化,和單一職責
處理異常時遵循的兩個 try-catch 經(jīng)驗法則:
- try-catch 應盡可能靠近錯誤的源頭
- 每個函數(shù)只使用一個 try-catch
他們將使你的代碼在短期和長期內更易于閱讀和維護??纯催@里的 processJSONFile() ??,它遵守了規(guī)則 1。
第一個 try-catch 僅負責處理文件讀取錯誤,沒有其他功能。不會再向 try 添加任何邏輯,所以 catch 也永遠不會改變。
接下來的 try-catch 就在這里處理 JSON 解析。
function processJSONFile(filePath) {
let contents;
let jsonContents;
// ? 第一個 try-catch 塊,用于處理文件讀取錯誤
try {
contents = fs.readFileSync(filePath, 'utf8');
} catch (error) {
// 在這里記錄錯誤
contents = null;
}
// ? 第二個 try-catch 塊,用于處理 JSON 解析錯誤
try {
jsonContents = JSON.parse(contents);
} catch (error) {
// 在這里記錄錯誤
jsonContents = null;
}
return jsonContents;
}
但是 processJsonFile() 完全無視規(guī)則 2,同一個函數(shù)中的 try-catch 塊都在。
那么,我們通過將它們重構為各自的函數(shù)來解決這個問題:
function processJSONFile(filePath) {
const contents = getFileContents(filePath);
const jsonContents = parseJSON(contents);
return jsonContents;
}
function getFileContents(filePath) {
let contents;
try {
contents = fs.readFileSync(filePath, 'utf8');
} catch (error) {
contents = null;
}
return contents;
}
function parseJSON(content) {
let json;
try {
json = JSON.parse(content);
} catch (error) {
json = null;
}
return json;
}
但是我們現(xiàn)在有 tryCatch() - 我們可以做得更好:
function processJSONFile(filePath) {
return parseJSON(getFileContents(filePath));
}
const getFileContents = (filePath) =>
tryCatch({
tryFn: () => fs.readFileSync(filePath, 'utf8'),
catchFn: () => null,
});
const parseJSON = (content) =>
tryCatch({
tryFn: () => JSON.parse(content),
catchFn: () => null,
});
我們正在做的只不過是消除異?!@就是這些新功能的主要工作。
如果這種情況經(jīng)常發(fā)生,為什么不創(chuàng)建一個“靜音器”版本,在成功時返回 try 函數(shù)的結果,或者在錯誤時什么也不返回?
function tryCatch<T>(fn: () => T) {
try {
return fn();
} catch (error) {
return null;
}
}
將我們的代碼進一步縮短為這樣:
function processJSONFile(filePath) {
return parseJSON(getFileContents(filePath));
}
const getFileContents = (filePath) =>
tryCatch(() => fs.readFileSync(filePath, 'utf8'));
const parseJSON = (content) =>
tryCatch(() => JSON.parse(content));
附注:在命名標識符時,我建議我們盡可能地使用名詞來表示變量,形容詞來表示函數(shù),而對于高階函數(shù)……我們可以使用副詞!就像一個故事,代碼將更自然地閱讀,并可能更好地理解。
所以,我們可以使用 silently ,而不是 tryCatch :
const getFileContents = (filePath) =>
silently(() => fs.readFileSync(filePath, 'utf8'));
const parseJSON = (content) =>
silently(() => JSON.parse(content));
總結
當然, try-catch 本身就能完美運行。
我們并沒有丟棄它,而是將其轉化為更易維護和可預測的工具。 tryCatch() 甚至只是許多使用像 try-catch 這樣的命令式構造的聲明式友好函數(shù)之一
如果更喜歡直接使用 try-catch ,請記住使用 2 個 try-catch 的經(jīng)驗法則,以提高您的代碼的模塊化和可讀性。