所以,让我们从关于我的一些背景故事开始。我是一位具有大约10年经验的软件开发人员,最初与PHP合作,然后逐渐过渡到JavaScript。另外,这是我有史以来的第一篇文章,所以请了解:)
我大约在5年前的某个地方开始使用Typescript,从那以后,我再也没有回到JavaScript。我开始使用它的那一刻,我认为这是有史以来最好的编程语言。每个人都喜欢它,每个人都使用它...这只是最好的,对吗?正确的?对吗?
是的,然后我开始与其他语言(更现代的语言)一起玩耍。首先是去,然后我慢慢将Rust添加到我的列表中(感谢Prime)。
当您不知道存在不同的事物时,很难错过事情。
我在说什么?什么常见的东西分享?错误。对我来说最突出的一件事。更具体地说,这些语言如何处理它们。
javaScript依靠投掷异常来处理错误,而生锈将它们视为值。您可能会认为这没什么大不了的……但是,男孩,这听起来像是一件琐碎的事情。但是,这是一个改变游戏规则的人。
让我们穿过它们。我们不会深入研究每种语言。我们只想知道一般方法。
让我们从javascript / tyspript和一个小游戏开始。< / p>
给自己5秒钟以查看下面的代码,并回答为什么我们需要将其包装在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;
}
没有错误,没有衬里,即使这可能会破坏您的应用。
现在在我的脑海中,我可以听到:“有什么问题,只需在任何地方尝试 /捕捉。”这是第三个问题;我们不知道哪一投。当然,我们可以以错误消息来以某种方式猜测,但是对于更大的服务 /功能,可能会发生错误吗?您确定您是否通过一次尝试 /捕获来正确处理所有这些?< / p>
好吧,是时候停止挑选JS并移至其他东西了。让我们从Go开始:
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
我们正在尝试打开一个文件,该文件正在返回文件或错误。您会看到很多东西,主要是因为我们知道哪些函数始终返回错误。你永远不会错过一个。这是将误差视为值的第一个示例。您指定哪些功能可以返回它们,返回它们,分配它们,检查它们,与它们一起工作。
它也不是那么色彩丰富,也是批评的事情之一,error-checking code
,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使用其惊人的枚举来处理错误(它们与打字稿枚举不同!)。不详细介绍,这里重要的是它使用了一个称为Result
的枚举,带有两个变体:Ok
和Err
。您可能会猜到,Ok
具有一个值,Err
保持...惊喜,错误:d。
它还有很多方法可以更方便地处理它们,以减轻GO问题。最著名的是?
操作员。
let greeting_file_result = File::open("hello.txt")?;
这里的摘要是,始终在哪里出现错误。他们迫使您在(大部分)出现的地方对其进行处理。没有隐藏的,没有猜测,没有带有惊喜面的破坏应用。
这种方法更好。一英里。
好吧,说实话,我有点撒谎了。我们不能像Go / Rust遇到的那样使打字稿错误奏效。这里的限制因素是语言本身。它只是没有适当的工具来做到这一点。
但是我们能做的就是尝试使其相似。并使其简单。
从此开始:
export type Safe<T> =
| {
success: true;
data: T;
}
| {
success: false;
error: string;
};
这里没有什么真正喜欢的,只是一种简单的通用类型。但是这个小婴儿可以完全更改代码。您可能会注意到,这里最大的区别是我们要么返回数据或错误。听起来很熟悉?
另外,第二个谎言,我们需要一些尝试 /捕捉。好事是我们只需要至少两个,而不是100000。
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" };
}
}
“哇,真是个天才,他为尝试 /捕捉创造了一个包装纸。”是的你是对的;这只是我们的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)
是的,我们的新解决方案更长,但是:
- 没有试用catch
- 我们处理发生的每个错误
- 我们可以为特定功能指定错误消息
- 我们有一个不错的自上而下逻辑,顶部所有错误,然后只有底部的响应
,但现在来了。如果我们忘了检查这个:
会发生什么:
if (!body.success) {
// handle error (body.error)
return;
}
事情是...我们不能。是的,我们必须进行检查;如果我们不这样做,那么body.data
将不存在。 LSP将通过在类型'Safe'上扔“属性'数据”来提醒我们。这全都归功于我们创建的简单的Safe
类型。而且它也适用于错误消息;在检查!body.success
之前,我们无法访问body.error
。
这是我们真正应该欣赏打字稿及其如何改变JavaScript世界的时刻。
也是如此:
if (!response.success) {
// handle error (response.error)
return;
}
我们无法删除!response.success
检查,因为否则response.data
将不存在。
当然,我们的解决方案并非没有问题,最大的解决方案是您需要记住要包装诺言 /功能,这些诺言 /功能可能会给我们的safe
包装提供错误。这个“我们需要知道”是我们无法克服的语言限制。
听起来可能很难,但事实并非如此。您很快就开始意识到,您在代码中拥有的几乎所有承诺都会丢弃错误,并且可以知道它们的同步功能,并且没有太多的函数。
,您可能会问,值得吗?我们认为是,并且在我们的团队中正常工作:)。当您查看较大的服务文件时,在任何地方都没有尝试 /捕捉,每个错误都在出现的位置处理,逻辑流很好...它看起来不错。< / p>
这是现实生活中的用法(Sveltekit表格):
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
帮助我们使用GRPC回调 - createMetadata返回
Safe
内部,所以我们不需要包装它。 -
zod
库使用相同的模式:)如果我们不进行schema.success
检查,我们将无法访问schema.data
。
看起来不干净吗?所以尝试一下!也许也很适合您:)
另外,我希望这篇文章对您很有趣。我希望创建更多的人分享我的想法和想法。
P.S。看起来相似吗?
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