JAVAScript 依靠拋出異常來處理錯誤,而 Go 和 Rust 將它們視為值。 你可能認為這沒什么大不了的……但是,孩子,這可能聽起來微不足道; 然而,它改變了游戲規則。
那么,讓我們從我的一些背景故事開始。 我是一名擁有大約十年經驗的軟件開發人員,最初使用 php,然后逐漸過渡到 JavaScript。
大約五年前,我開始使用 TypeScript,從那時起,我就再也沒有回到過 JavaScript。 當我開始使用它的那一刻,我認為它是有史以來最好的編程語言。 每個人都喜歡它; 每個人都用它……這只是最好的,對吧? 正確的? 正確的?
是的,然后我開始嘗試其他語言,更現代的語言。 首先是 Go,然后我慢慢地將 Rust 添加到我的列表中(感謝 Prime)。
當您不知道不同事物的存在時,就很難錯過事物。
我在說什么? Go 和 Rust 的共同點是什么? 錯誤。 對我來說最突出的事情。 更具體地說,這些語言如何處理它們。
JavaScript 依靠拋出異常來處理錯誤,而 Go 和 Rust 將它們視為值。 你可能認為這沒什么大不了的……但是,孩子,這可能聽起來微不足道; 然而,它改變了游戲規則。
讓我們來看看它們。 我們不會深入研究每種語言; 我們想知道一般方法。
讓我們從 JavaScript/TypeScript 和一個小游戲開始。
給自己五秒鐘的時間來查看下面的代碼并回答為什么我們需要將其包裝在 try/catch 中。
try {
const request = { name: “test”, value: 2n };
const body = JSON.stringify(request);
const response = awAIt fetch("https://example.com", {
method: “POST”,
body,
});
if (!response.ok) {
return;
}
// handle response
} catch (e) {
// handle error
return;
}
所以,我假設你們大多數人都猜到即使我們正在檢查response.ok,fetch 方法仍然會拋出錯誤。 response.ok 僅“捕獲”4xx 和 5xx 網絡錯誤。 但當網絡本身出現故障時,就會拋出錯誤。
但我想知道有多少人猜到 JSON.stringify 也會拋出錯誤。 原因是請求對象包含bigint(2n)變量,JSON不知道如何字符串化。
所以第一個問題是,就我個人而言,我認為這是有史以來最大的 JavaScript 問題:我們不知道什么會引發錯誤。 從 JavaScript 錯誤的角度來看,它與以下內容相同:
try {
let data = “Hello”;
} catch (err) {
console.error(err);
}
JavaScript 不知道; JavaScript 不在乎。 你應該知道。
第二件事,這是完全可行的代碼:
const request = { name: “test”, value: 2n };
const body = JSON.stringify(request);
const response = await fetch("https://example.com", {
method: “POST”,
body,
});
if (!response.ok) {
return;
}
沒有錯誤,即使這可能會破壞您的應用程序。
現在,在我的腦海中,我可以聽到,“有什么問題,只要在任何地方使用 try/catch 就可以了。” 第三個問題來了:我們不知道拋出的是哪一個。 當然,我們可以通過錯誤消息進行猜測,但是對于有很多可能發生錯誤的地方的更大的服務/功能呢? 您確定通過一次 try/catch 正確處理了所有這些問題嗎?
好吧,是時候停止對 JS 的挑剔,轉向其他的事情了。 讓我們從這段 Go 代碼開始:
f, err := os.Open(“filename.ext”)
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
我們正在嘗試打開一個返回文件或錯誤的文件。 您會經常看到這種情況,主要是因為我們知道哪些函數總是返回錯誤。 你永遠不會錯過任何一個。 這是將錯誤視為值的第一個示例。 您指定哪個函數可以返回它們,您返回它們,您分配它們,您檢查它們,您使用它們。
它也沒有那么豐富多彩,這也是 Go 受到批評的事情之一——“錯誤檢查代碼”,其中 if err != nil { .... 有時需要比其他代碼行更多的代碼。
if err != nil {
…
if err != nil {
…
if err != nil {
…
}
}
}
if err != nil {
…
}
…
if err != nil {
…
}
仍然完全值得付出努力,相信我。
最后,鐵銹:
let greeting_file_result = File::open(“hello.txt”);
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
這里顯示的三個中最冗長的一個,具有諷刺意味的是,也是最好的一個。 因此,首先,Rust 使用其令人驚嘆的枚舉來處理錯誤(它們與 TypeScript 枚舉不同!)。 無需詳細介紹,這里重要的是它使用一個名為 Result 的枚舉,它有兩個變體:Ok 和 Err。 正如您可能猜到的,Ok 保存一個值,Err 保存……令人驚訝的是,一個錯誤:D。
它還有很多方法可以更方便地處理它們,以緩解 Go 問題。 最知名的是? 操作員。
let greeting_file_result = File::open(“hello.txt”)?;
這里的總結是,Go 和 Rust 總是知道哪里可能出現錯誤。 它們迫使你在它出現的地方(大部分)處理它。 沒有隱藏的,沒有猜測,沒有令人驚訝的面孔破壞應用程序。
而且這種方法更好。 一英里。
好吧,是時候說實話了; 我撒了一點謊。 我們不能讓 TypeScript 錯誤像 Go / Rust 那樣工作。 這里的限制因素是語言本身; 它沒有合適的工具來做到這一點。
但我們能做的就是盡量讓它相似。 并使其變得簡單。
從這個開始:
export type Safe<T> =
| {
success: true;
data: T;
}
| {
success: false;
error: string;
};
這里沒什么特別的,只是一個簡單的泛型類型。 但這個小寶貝可以完全改變代碼。 您可能會注意到,這里最大的區別是我們要么返回數據,要么返回錯誤。 聽起來很熟悉?
另外……第二個謊言,我們確實需要一些嘗試/捕獲。 好消息是我們只需要大約兩個,而不是 100,000 個。
export function safe<T>(promise: Promise<T>, err?: string): Promise<Safe<T>>;
export function safe<T>(func: () => T, err?: string): Safe<T>;
export function safe<T>(
promiseorFunc: Promise<T> | (() => T),
err?: string,
): Promise<Safe<T>> | Safe<T> {
if (promiseOrFunc instanceof Promise) {
return safeAsync(promiseOrFunc, err);
}
return safeSync(promiseOrFunc, err);
}
async function safeAsync<T>(
promise: Promise<T>,
err?: string
): Promise<Safe<T>> {
try {
const data = await promise;
return { data, success: true };
} catch (e) {
console.error(e);
if (err !== undefined) {
return { success: false, error: err };
}
if (e instanceof Error) {
return { success: false, error: e.message };
}
return { success: false, error: "Something went wrong" };
}
}
function safeSync<T>(
func: () => T,
err?: string
): Safe<T> {
try {
const data = func();
return { data, success: true };
} catch (e) {
console.error(e);
if (err !== undefined) {
return { success: false, error: err };
}
if (e instanceof Error) {
return { success: false, error: e.message };
}
return { success: false, error: "Something went wrong" };
}
}
“哇哦,真是個天才。 他為 try/catch 創建了一個包裝器。” 是的你是對的; 這只是一個包裝器,以我們的 Safe 類型作為返回類型。 但有時您所需要的只是簡單的事情。 讓我們將它們與上面的示例結合起來。
舊的(16行):
try {
const request = { name: “test”, value: 2n };
const body = JSON.stringify(request);
const response = await fetch("https://example.com", {
method: “POST”,
body,
});
if (!response.ok) {
// handle.NETwork error
return;
}
// handle response
} catch (e) {
// handle error
return;
}
新的(20行):
const request = { name: “test”, value: 2n };
const body = safe(
() => JSON.stringify(request),
“Failed to serialize request”,
);
if (!body.success) {
// handle error (body.error)
return;
}
const response = await safe(
fetch("https://example.com", {
method: “POST”,
body: body.data,
}),
);
if (!response.success) {
// handle error (response.error)
return;
}
if (!response.data.ok) {
// handle network error
return;
}
// handle response (body.data)
所以,是的,我們的新解決方案更長,但性能更好,原因如下:
- 沒有try/catch。
- 我們處理發生的每個錯誤。
- 我們可以為特定函數指定錯誤消息。
- 我們有一個很好的從上到下的邏輯,所有錯誤都在頂部,然后只有響應在底部。
但現在王牌來了。 如果我們忘記檢查這一點會發生什么:
if (!body.success) {
// handle error (body.error)
return;
}
問題是……我們不能。 是的,我們必須進行這項檢查。 如果不這樣做,body.data 將不存在。 LSP 將通過拋出“‘Safe<string>’類型上不存在屬性‘data’”錯誤來提醒我們。 這一切都歸功于我們創建的簡單 Safe 類型。 它也適用于錯誤消息。 在檢查 !body.success 之前,我們無法訪問 body.error。
現在我們應該欣賞 TypeScript 以及它如何改變 JavaScript 世界。
以下內容也是如此:
if (!response.success) {
// handle error (response.error)
return;
}
我們不能刪除 !response.success 檢查,因為否則,response.data 將不存在。
當然,我們的解決方案并非沒有問題。 最重要的一點是,您必須記住使用我們的安全包裝器來包裝可能引發錯誤的 Promise/函數。 這種“我們需要知道”是我們無法克服的語言限制。
聽起來可能很難,但事實并非如此。 您很快就會開始意識到,代碼中幾乎所有的 Promise 都可能會拋出錯誤,而同步函數也會拋出錯誤,您知道它們,但它們并不多。
不過,您可能會問,值得嗎? 我們認為是的,而且它在我們的團隊中運行得很好:)。 當您查看更大的服務文件時,任何地方都沒有 try/catch,每個錯誤都在出現的地方進行處理,具有良好的邏輯流程……它看起來不錯。
以下是使用 SvelteKit Formaction 的真實示例:
export const actions = {
createEmail: async ({ locals, request }) => {
const end = perf(“CreateEmail”);
const form = await safe(request.formData());
if (!form.success) {
return fail(400, { error: form.error });
}
const schema = z
.object({
emailTo: z.string().email(),
emailName: z.string().min(1),
emailSubject: z.string().min(1),
emailhtml: z.string().min(1),
})
.safeParse({
emailTo: form.data.get("emailTo"),
emailName: form.data.get("emailName"),
emailSubject: form.data.get("emailSubject"),
emailHtml: form.data.get("emailHtml"),
});
if (!schema.success) {
console.error(schema.error.flatten());
return fail(400, { form: schema.error.flatten().fieldErrors });
}
const metadata = createMetadata(URI_GRPC, locals.user.key)
if (!metadata.success) {
return fail(400, { error: metadata.error });
}
const response = await new Promise<Safe<Email__Output>>((res) => {
usersClient.createEmail(schema.data, metadata.data, grpcSafe(res));
});
if (!response.success) {
return fail(400, { error: response.error });
}
end();
return {
email: response.data,
};
},
} satisfies Actions;
這里有幾點需要指出:
- 我們的自定義函數 grpcSafe 幫助我們處理 gGRPC 回調。
- createMetadata 在內部返回 Safe,所以我們不需要包裝它。
- zod 庫使用相同的模式:) 如果我們不進行 schema.success 檢查,我們就無法訪問 schema.data。
是不是看起來很干凈呢? 所以嘗試一下吧! 也許它也非常適合您:)
謝謝閱讀。
附: 看起來很相似?
f, err := os.Open(“filename.ext”)
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
const response = await safe(fetch(“https://example.com"));
if (!response.success) {
console.error(response.error);
return;
}
// do something with the response.data