发布于 2025-12-10 1 阅读
0

TypeScript 报 Go/Rust 错误?没有 try/catch 吗?异端邪说。完

TypeScript 出现 Go/Rust 错误?没有 try/catch 吗?简直是异端邪说。

结尾

那么,我们先来聊聊我自己。我是一名软件开发者,拥有大约 10 年的经验,最初使用 PHP,然后逐渐过渡到 JavaScript。另外,这是我的第一篇文章,所以请大家谅解 :)

我大约五年前开始使用 TypeScript,从那以后就再也没有用过 JavaScript。一开始用的时候,我就觉得它是有史以来最好的编程语言。每个人都喜欢它,每个人都在用它……它就是最好的,对吧?对吧?对吧?

是的,后来我开始尝试其他语言,一些更现代的语言。最开始是 Go,后来慢慢地把 Rust 也加到我的列表中(感谢 Prime)。

当你不知道不同事物的存在时,很难错过它们。

我在说什么?Go 和 Rust 有什么共同点?错误。对我来说,最突出的一点。更具体地说,这些语言是如何处理错误的。

JavaScript 依赖于抛出异常来处理错误,而 Go 和 Rust 则将错误视为值。你可能觉得这没什么大不了的……但是,这听起来可能微不足道;然而,它却改变了游戏规则。

让我们来了解一下这些语言。我们不会深入研究每一种语言;我们只想了解一下它们的通用方法。

让我们从 JavaScript / TypeScript 和一个小游戏开始。

给自己 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;
}
Enter fullscreen mode Exit fullscreen mode

所以,我想你们大多数人都猜到了,即使我们检查了response.ok,fetch 方法仍然可能抛出错误。它response.ok只能“捕获” 4xx 和 5xx 网络错误。但是当网络本身出现故障时,它会抛出错误。

但我不知道你们有多少人猜到了,这JSON.stringify也会抛出一个错误。原因是请求对象包含一个bigint (2n)变量,而这个变量JSON不知道如何进行字符串化。

所以第一个问题是,我个人认为这是 JavaScript 有史以来最大的问题:我们不知道什么会抛出错误。从 JavaScript 错误的角度来看,它等同于:

try {
    let data = "Hello";
} catch (err) {
    console.error(err);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

没有错误,没有 linters,尽管这可能会破坏您的应用程序。

现在我脑子里都在想:“这有什么问题,到处都用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
Enter fullscreen mode Exit fullscreen mode

我们正在尝试打开一个文件,它返回的要么是文件,要么是错误。你会经常看到这种情况,主要是因为我们总是知道哪些函数会返回错误。你绝不会错过任何一个错误。这是将错误视为值的第一个示例。你可以指定哪个函数可以返回它们,然后返回它们、对它们赋值、检查它们,以及使用它们。

它也不是很丰富多彩,这也是 Go 受到批评的原因之一,有时error-checking codeif err != nil { ....比其他代码需要更多的代码行。

if err != nil {
    ...
    if err != nil {
        ...
        if err != nil {
            ... 
        }
    }  
}
if err != nil {
    ... 
}
...
if err != nil {
    ... 
}
Enter fullscreen mode Exit fullscreen mode

仍然值得付出努力,相信我。

最后是 Rust:

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),
};
Enter fullscreen mode Exit fullscreen mode

这里展示的三个方法中最冗长的,讽刺的是,也是最好的一个。首先,Rust 使用其神奇的枚举来处理错误(它们与 TypeScript 枚举不同!)。无需赘述,这里重要的是它使用了一个枚举,Result它有两个变量:OkErr。正如你可能猜到的,Ok保存一个值,并且Err保存……惊喜的是,一个错误 :D。

Go 也提供了很多更便捷的方法来解决这些问题,其中最广为人知的就是?运算符。

let greeting_file_result = File::open("hello.txt")?;
Enter fullscreen mode Exit fullscreen mode

总结一下,Go 和 Rust 总是知道哪里可能出现错误。而且它们会强制你在错误出现的地方(大多数情况下)进行处理。没有隐藏的错误,没有猜测,也不会因为意外而导致应用程序崩溃。

这种方法确实好得多。好得惊人。

好吧,该说实话了,我撒了点谎。我们无法让 TypeScript 的错误像 Go/Rust 那样工作。这里限制因素是语言本身;它没有合适的工具来实现这一点。

但我们能做的就是尽量让它们变得相似,并且简单。

从这个开始:

export type Safe<T> =
    | {
          success: true;
          data: T;
      }
    | {
          success: false;
          error: string;
      };
Enter fullscreen mode Exit fullscreen mode

这里没什么特别的,只是一个简单的泛型。但这个小东西可以完全改变代码。你可能注意到了,这里最大的区别是我们要么返回数据,要么返回错误。听起来很熟悉吧?

另外,第二个谎言是,我们需要一些 try/catch 语句。好消息是,我们至少只需要两个,而不是 100000 个。

export function safe<T>(promise: Promise<T>): Promise<Safe<T>>;
export function safe<T>(func: () => T): Safe<T>;
export function safe<T>(
    promiseOrFunc: Promise<T> | (() => T),
): Promise<Safe<T>> | Safe<T> {
    if (promiseOrFunc instanceof Promise) {
        return safeAsync(promiseOrFunc);
    }
    return safeSync(promiseOrFunc);
}

async function safeAsync<T>(promise: Promise<T>): Promise<Safe<T>> {
    try {
        const data = await promise;
        return { data, success: true };
    } catch (e) {
        console.error(e);
        if (e instanceof Error) {
            return { success: false, error: e.message };
        }
        return { success: false, error: "Something went wrong" };
    }
}

function safeSync<T>(func: () => T): Safe<T> {
    try {
        const data = func();
        return { data, success: true };
    } catch (e) {
        console.error(e);
        if (e instanceof Error) {
            return { success: false, error: e.message };
        }
        return { success: false, error: "Something went wrong" };
    }
}
Enter fullscreen mode Exit fullscreen mode

“哇,真是个天才,他居然为 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;
}
Enter fullscreen mode Exit fullscreen mode

新的(20行):

const request = { name: "test", value: 2n };
const body = safe(() => JSON.stringify(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)
Enter fullscreen mode Exit fullscreen mode

是的,我们的新解决方案更长,但是:

  • 无需尝试捕获
  • 我们会处理发生的每个错误
  • 我们有一个很好的自上而下的逻辑,所有错误都放在顶部,然后只有响应放在底部

但现在王牌来了。如果我们忘记检查这一点,会发生什么:

if (!body.success) {
    // handle error (body.error)
    return;
}
Enter fullscreen mode Exit fullscreen mode

问题是……我们不能。是的,我们必须进行这项检查;如果不做,那么 就body.data不会存在。LSP 会抛出“属性‘data’在类型‘Safe’上不存在”的错误来提醒我们。这全都归功于我们创建的简单类型。它也适用于错误消息;在我们检查 之前,Safe我们无法访问body.error!body.success

此时此刻,我们应该真正欣赏 TypeScript 以及它如何改变 JavaScript 世界。

以下情况同样如此:

if (!response.success) {
    // handle error (response.error)
    return;
}
Enter fullscreen mode Exit fullscreen mode

我们不能取消该!response.success检查,因为否则它将response.data不复存在。

当然,我们的解决方案并非没有问题,最大的问题是你需要记住用我们的safe包装器来包装那些可能抛出错误的 Promises / 函数。这个“我们需要知道”的问题是语言的限制,我们无法克服。

这听起来可能很难,但其实不然。你很快就会意识到,代码中几乎所有的 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;
Enter fullscreen mode Exit fullscreen mode

需要指出的几点:

  • 我们的自定义函数grpcSafe可以帮助我们进行 grpc 回调
  • createMetadata 在里面返回Safe,所以我们不需要包装它。
  • zod库使用相同的模式:)如果我们不检查schema.success,我们就无法访问schema.data

看起来是不是干净利落?快来试试吧!说不定你也会喜欢 :)

另外,我希望这篇文章能让你感兴趣。我希望创作更多类似的文章来分享我的想法和观点。

PS 看起来很相似?

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f
Enter fullscreen mode Exit fullscreen mode
const response = await safe(fetch("https://example.com"));
if (!response.success) {
    console.error(response.error);
    return;
}
// do something with the response.data
Enter fullscreen mode Exit fullscreen mode

结尾

希望你喜欢它!

像往常一样,进行一点自我推销:)

关注我的GithubTwitter 账号,即可收到最新通知。我致力于推广一些鲜为人知的技术,主要关注 Svelte、Go 和 Rust。

我也是GoFast的开发者,它是利用 Golang 和 SvelteKit / Next.js 的强大功能构建现代应用程序的终极基础。它也充分利用了这里描述的所有内容。如果你感兴趣的话,可以加入进来 :)

鏂囩珷鏉ユ簮锛�https://dev.to/mpiorowski/typescript-with-gorust-errors-no-trycatch-heresy-49mf