TypeScript 出現(xiàn) Go 和 Rust的 錯(cuò)誤? 沒(méi)有Try/Catch?
那么,讓我們從我的一些背景故事開始。 我是一名擁有大約十年經(jīng)驗(yàn)的軟件開發(fā)人員,最初使用 PHP,然后逐漸過(guò)渡到 JavaScript。
大約五年前,我開始使用 TypeScript,從那時(shí)起,我就再也沒(méi)有回到過(guò) JavaScript。 當(dāng)我開始使用它的那一刻,我認(rèn)為它是有史以來(lái)最好的編程語(yǔ)言。 每個(gè)人都喜歡它; 每個(gè)人都用它……這只是最好的,對(duì)吧? 正確的? 正確的?
是的,然后我開始嘗試其他語(yǔ)言,更現(xiàn)代的語(yǔ)言。 首先是 Go,然后我慢慢地將 Rust 添加到我的列表中(感謝 Prime)。
當(dāng)您不知道不同事物的存在時(shí),就很難錯(cuò)過(guò)事物。
我在說(shuō)什么? Go 和 Rust 的共同點(diǎn)是什么? 錯(cuò)誤。 對(duì)我來(lái)說(shuō)最突出的事情。 更具體地說(shuō),這些語(yǔ)言如何處理它們。
JavaScript 依靠拋出異常來(lái)處理錯(cuò)誤,而 Go 和 Rust 將它們視為值。 你可能認(rèn)為這沒(méi)什么大不了的……但是,孩子,這可能聽起來(lái)微不足道; 然而,它改變了游戲規(guī)則。
讓我們來(lái)看看它們。 我們不會(huì)深入研究每種語(yǔ)言; 我們想知道一般方法。
讓我們從 JavaScript/TypeScript 和一個(gè)小游戲開始。
給自己五秒鐘的時(shí)間來(lái)查看下面的代碼并回答為什么我們需要將其包裝在 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;
}
所以,我假設(shè)你們大多數(shù)人都猜到即使我們正在檢查response.ok,fetch 方法仍然會(huì)拋出錯(cuò)誤。 response.ok 僅“捕獲”4xx 和 5xx 網(wǎng)絡(luò)錯(cuò)誤。 但當(dāng)網(wǎng)絡(luò)本身出現(xiàn)故障時(shí),就會(huì)拋出錯(cuò)誤。
但我想知道有多少人猜到 JSON.stringify 也會(huì)拋出錯(cuò)誤。 原因是請(qǐng)求對(duì)象包含bigint(2n)變量,JSON不知道如何字符串化。
所以第一個(gè)問(wèn)題是,就我個(gè)人而言,我認(rèn)為這是有史以來(lái)最大的 JavaScript 問(wèn)題:我們不知道什么會(huì)引發(fā)錯(cuò)誤。 從 JavaScript 錯(cuò)誤的角度來(lái)看,它與以下內(nèi)容相同:
try {
let data = “Hello”;
} catch (err) {
console.error(err);
}
JavaScript 不知道; JavaScript 不在乎。 你應(yīng)該知道。
第二件事,這是完全可行的代碼:
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;
}
沒(méi)有錯(cuò)誤,即使這可能會(huì)破壞您的應(yīng)用程序。
現(xiàn)在,在我的腦海中,我可以聽到,“有什么問(wèn)題,只要在任何地方使用 try/catch 就可以了?!?第三個(gè)問(wèn)題來(lái)了:我們不知道拋出的是哪一個(gè)。 當(dāng)然,我們可以通過(guò)錯(cuò)誤消息進(jìn)行猜測(cè),但是對(duì)于有很多可能發(fā)生錯(cuò)誤的地方的更大的服務(wù)/功能呢? 您確定通過(guò)一次 try/catch 正確處理了所有這些問(wèn)題嗎?
好吧,是時(shí)候停止對(duì) JS 的挑剔,轉(zhuǎn)向其他的事情了。 讓我們從這段 Go 代碼開始:
f, err := os.Open(“filename.ext”)
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
我們正在嘗試打開一個(gè)返回文件或錯(cuò)誤的文件。 您會(huì)經(jīng)??吹竭@種情況,主要是因?yàn)槲覀冎滥男┖瘮?shù)總是返回錯(cuò)誤。 你永遠(yuǎn)不會(huì)錯(cuò)過(guò)任何一個(gè)。 這是將錯(cuò)誤視為值的第一個(gè)示例。 您指定哪個(gè)函數(shù)可以返回它們,您返回它們,您分配它們,您檢查它們,您使用它們。
它也沒(méi)有那么豐富多彩,這也是 Go 受到批評(píng)的事情之一——“錯(cuò)誤檢查代碼”,其中 if err != nil { .... 有時(shí)需要比其他代碼行更多的代碼。
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),
};
這里顯示的三個(gè)中最冗長(zhǎng)的一個(gè),具有諷刺意味的是,也是最好的一個(gè)。 因此,首先,Rust 使用其令人驚嘆的枚舉來(lái)處理錯(cuò)誤(它們與 TypeScript 枚舉不同!)。 無(wú)需詳細(xì)介紹,這里重要的是它使用一個(gè)名為 Result 的枚舉,它有兩個(gè)變體:Ok 和 Err。 正如您可能猜到的,Ok 保存一個(gè)值,Err 保存……令人驚訝的是,一個(gè)錯(cuò)誤:D。
它還有很多方法可以更方便地處理它們,以緩解 Go 問(wèn)題。 最知名的是? 操作員。
let greeting_file_result = File::open(“hello.txt”)?;
這里的總結(jié)是,Go 和 Rust 總是知道哪里可能出現(xiàn)錯(cuò)誤。 它們迫使你在它出現(xiàn)的地方(大部分)處理它。 沒(méi)有隱藏的,沒(méi)有猜測(cè),沒(méi)有令人驚訝的面孔破壞應(yīng)用程序。
而且這種方法更好。 一英里。
好吧,是時(shí)候說(shuō)實(shí)話了; 我撒了一點(diǎn)謊。 我們不能讓 TypeScript 錯(cuò)誤像 Go / Rust 那樣工作。 這里的限制因素是語(yǔ)言本身; 它沒(méi)有合適的工具來(lái)做到這一點(diǎn)。
但我們能做的就是盡量讓它相似。 并使其變得簡(jiǎn)單。
從這個(gè)開始:
export type Safe<T> =
| {
success: true;
data: T;
}
| {
success: false;
error: string;
};
這里沒(méi)什么特別的,只是一個(gè)簡(jiǎn)單的泛型類型。 但這個(gè)小寶貝可以完全改變代碼。 您可能會(huì)注意到,這里最大的區(qū)別是我們要么返回?cái)?shù)據(jù),要么返回錯(cuò)誤。 聽起來(lái)很熟悉?
另外……第二個(gè)謊言,我們確實(shí)需要一些嘗試/捕獲。 好消息是我們只需要大約兩個(gè),而不是 100,000 個(gè)。
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" };
}
}
“哇哦,真是個(gè)天才。 他為 try/catch 創(chuàng)建了一個(gè)包裝器?!?是的你是對(duì)的; 這只是一個(gè)包裝器,以我們的 Safe 類型作為返回類型。 但有時(shí)您所需要的只是簡(jiǎn)單的事情。 讓我們將它們與上面的示例結(jié)合起來(lái)。
舊的(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)
所以,是的,我們的新解決方案更長(zhǎng),但性能更好,原因如下:
- 沒(méi)有try/catch。
- 我們處理發(fā)生的每個(gè)錯(cuò)誤。
- 我們可以為特定函數(shù)指定錯(cuò)誤消息。
- 我們有一個(gè)很好的從上到下的邏輯,所有錯(cuò)誤都在頂部,然后只有響應(yīng)在底部。
但現(xiàn)在王牌來(lái)了。 如果我們忘記檢查這一點(diǎn)會(huì)發(fā)生什么:
if (!body.success) {
// handle error (body.error)
return;
}
問(wèn)題是……我們不能。 是的,我們必須進(jìn)行這項(xiàng)檢查。 如果不這樣做,body.data 將不存在。 LSP 將通過(guò)拋出“‘Safe<string>’類型上不存在屬性‘data’”錯(cuò)誤來(lái)提醒我們。 這一切都?xì)w功于我們創(chuàng)建的簡(jiǎn)單 Safe 類型。 它也適用于錯(cuò)誤消息。 在檢查 !body.success 之前,我們無(wú)法訪問(wèn) body.error。
現(xiàn)在我們應(yīng)該欣賞 TypeScript 以及它如何改變 JavaScript 世界。
以下內(nèi)容也是如此:
if (!response.success) {
// handle error (response.error)
return;
}
我們不能刪除 !response.success 檢查,因?yàn)榉駝t,response.data 將不存在。
當(dāng)然,我們的解決方案并非沒(méi)有問(wèn)題。 最重要的一點(diǎn)是,您必須記住使用我們的安全包裝器來(lái)包裝可能引發(fā)錯(cuò)誤的 Promise/函數(shù)。 這種“我們需要知道”是我們無(wú)法克服的語(yǔ)言限制。
聽起來(lái)可能很難,但事實(shí)并非如此。 您很快就會(huì)開始意識(shí)到,代碼中幾乎所有的 Promise 都可能會(huì)拋出錯(cuò)誤,而同步函數(shù)也會(huì)拋出錯(cuò)誤,您知道它們,但它們并不多。
不過(guò),您可能會(huì)問(wèn),值得嗎? 我們認(rèn)為是的,而且它在我們的團(tuán)隊(duì)中運(yùn)行得很好:)。 當(dāng)您查看更大的服務(wù)文件時(shí),任何地方都沒(méi)有 try/catch,每個(gè)錯(cuò)誤都在出現(xiàn)的地方進(jìn)行處理,具有良好的邏輯流程……它看起來(lái)不錯(cuò)。
以下是使用 SvelteKit FormAction 的真實(shí)示例:
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;
這里有幾點(diǎn)需要指出:
- 我們的自定義函數(shù) grpcSafe 幫助我們處理 gGRPC 回調(diào)。
- createMetadata 在內(nèi)部返回 Safe,所以我們不需要包裝它。
- zod 庫(kù)使用相同的模式:) 如果我們不進(jìn)行 schema.success 檢查,我們就無(wú)法訪問(wèn) schema.data。
是不是看起來(lái)很干凈呢? 所以嘗試一下吧! 也許它也非常適合您:)
謝謝閱讀。
附: 看起來(lái)很相似?
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