JS、Go、Rust 错误处理的不同 - JS 可以不用 Try/Catch 吗?

news2024/9/21 5:37:39

在这里插入图片描述

原文:Mateusz Piorowski - 2023.07.24

先来了解一下我的背景吧。我是一名软件开发人员,有大约十年的工作经验,最初使用 PHP,后来逐渐转向 JavaScript。

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

是的,然后我开始接触其他的语言,更现代化的语言。首先是 Go,然后我慢慢地把 Rust 也加了进来。

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

我在说什么?Go 和 Rust 的共同点是什么?Error,这是最让我印象深刻的一点。更具体地说,这些语言是如何处理错误的。

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;
  }
  // 处理响应
} catch (e) {
  // 处理错误
  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)
}
// 对打开的 *File f 进行一些操作

我们正在尝试打开一个返回文件或错误的文件。你会经常看到这种情况,主要是因为我们知道哪些函数总是返回错误,你绝不会错过任何一个。这是第一个将错误视为值的例子。你可以指定哪个函数可以返回错误值,然后返回错误值,分配错误值,检查错误值,处理错误值。

这也是 Go 被诟病的地方之一——“错误检查代码”,其中 if err != nil { … 有时候的代码行数比其他部分还要多。

if err != nil {if err != nil {if err != nil {}
  }
}
if err != nil {}if err != nil {}

尽管如此,相信我,这些努力还是值得的。

最后,让我们看看 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),
};

这里显示的是三种错误处理中最冗长的一种,具有讽刺意味的是,它也是最好的一种。首先,Rust 使用其神奇的枚举(它们与 TypeScript 的枚举不同!)来处理错误。这里无需赘述,重要的是它使用了一个名为 Result 的枚举,有两个变量:OkErr。你可能已经猜到,Ok 包含一个值,而 Err 包含……没错,一个错误 😄。

它也有很多更方便的处理方式来缓解 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;
      };

这里没有什么花哨的东西,只是一个简单的通用类型。但这个小东西却能彻底改变代码。你可能会注意到,这里最大的不同就是我们要么返回数据,要么返回错误。听起来熟悉吗?

另外…第二个谎言是,我们确实需要一些 try/catch。好在我们只需要两个,而不是十万个。

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) {
    // 处理网络错误
    return;
  }
  // 处理响应
} catch (e) {
  // 处理错误
  return;
}

新的(20 行)示例:

const request = { name: “test”, value: 2n };
const body = safe(
  () => JSON.stringify(request),
  “Failed to serialize request”,
);
if (!body.success) {
  // 处理错误(body.error)
  return;
}
const response = await safe(
  fetch("https://example.com", {
    method:POST,
    body: body.data,
  }),
);
if (!response.success) {
  // 处理错误(response.error)
  return;
}
if (!response.data.ok) {
  // 处理网络错误
  return;
}
// 处理响应(body.data)

是的,我们的新解决方案更长,但性能更好,原因如下:

  • 没有 try/catch
  • 我们在错误发生的地方处理每个错误
  • 我们可以为特定函数指定一个错误信息
  • 我们有一个很好的自上而下的逻辑,所有错误都在顶部,然后底部只有响应

但现在王牌来了,如果我们忘记检查这个:

if (!body.success) {
    // 处理错误 (body.error)
    return;
}

事实是……我们不能忘记。是的,我们必须进行这个检查。如果我们不这样做,body.data 将不存在。LSP 会通过抛出 “Property ‘data’ does not exist on type ‘Safe’” 错误来提醒我们。这都要归功于我们创建的简单的 Safe 类型。它同样适用于错误信息,我们在检查 !body.success 之前无法访问 body.error

这是我们应该欣赏 TypeScript 以及它如何改变 JavaScript 世界的时刻。

以下也同样适用:

if (!response.success) {
    // 处理错误 (response.error)
    return;
}

我们不能移除 !response.success 检查,否则,response.data 将不存在。

当然,我们的解决方案也不是没有问题。最大的问题是你必须记住要用我们的 safe 包装器包装可能抛出异常的 Promise/函数。这个 “我们需要知道” 是我们无法克服的语言限制。

这听起来很难,但其实并不难。你很快就会意识到,你代码中的几乎所有 Promises 都会出错,而那些会出错的同步函数你也知道,而且它们的数量并不多。

不过,你可能会问,这样做值得吗?我们认为值得,而且在我们团队中运行得非常好:)。当你看到一个更大的服务文件,没有任何 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)
}
// 使用打开的 *File f 做一些事情
const response = await safe(fetch(“https://example.com"));
if (!response.success) {
  console.error(response.error);
  return;
}
// 使用 response.data 做一些事情

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1685824.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

10、SpringBoot 源码分析 - 自动配置深度分析三

SpringBoot 源码分析 - 自动配置深度分析三 refresh和自动配置大致流程AutoConfigurationImportSelector的getAutoConfigurationEntry获取自动配置实体(重点)AutoConfigurationImportSelector的getCandidateConfigurations获取EnableAutoConfiguration类型的名字集合AutoConfig…

一个超级简单的Python UI库:NiceGUI

大家好&#xff0c;图形用户界面&#xff08;GUI&#xff09;的开发往往被看作是一项复杂且繁琐的任务。Python作为一门强大的编程语言&#xff0c;提供了许多优秀的库来帮助开发者实现这一目标。今天&#xff0c;我们将介绍一个名为NiceGUI的库&#xff0c;它能帮助你轻松构建…

基于Tensorflow+Keras的卷积神经网络(CNN)人脸识别

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景与意义 人脸识别是计算机视觉领域的一个重要研究方向&#xff0c;广泛应用于安全监控、身份验证、人机…

弘君资本股市资讯:深夜,利空突袭!美股大跳水!

昨夜&#xff0c;两大利空音讯突袭美股。 隔夜&#xff0c;最新披露的两则数据引发商场忧虑。其一是&#xff0c;美国5月Markit归纳PMI初值报54.4&#xff0c;创25个月新高&#xff0c;大幅高于预期的51.2&#xff0c;前值为51.3&#xff1b;其二是&#xff0c;在到5月18日的一…

05Django项目--前端公共文件和其他html文件的适配

对应视频链接点击直达 Django项目--前端公共文件和其他html文件的适配 对应视频链接点击直达前端页面准备Pubilc页面设置 OVER&#xff0c;不会有人不会吧不会的加Q1394006513结语 接着上一节 前端页面准备 为了方便大家更好的复制&#xff0c;我把整个项目及templates文件夹和…

docker如何拉取redis最新镜像并运行

要拉取Docker Hub上最新版本的Redis镜像&#xff0c;您可以使用以下命令&#xff1a; docker pull redis:latest 这里的latest标签会自动获取Redis镜像的最新版本。如果您希望指定一个确切的版本号&#xff0c;可以直接使用该版本号替换latest。例如&#xff0c;要拉取Redis版…

C++ (week5):Linux系统编程2:进程

文章目录 二、进程1.CPU的虚拟化2.进程命令(1)ps 3.进程的基本操作 (API)(1)获取进程的标识 (获得进程id)&#xff1a;getpid、getppid(2)创建进程&#xff1a;fork()(3)终止进程&#xff1a;exit()、_exit()、abort()、wait()、waitpid()①正常终止&#xff1a;exit()、_exit(…

Redis 源码学习记录:散列 (dict)

散列 Redis 源码版本&#xff1a;Redis-6.0.9&#xff0c;本篇文章的代码均在 dict.h / dict.c 文件中。 散列类型可以存储一组无需的键值对&#xff0c;他特别适用于存储一个对象数据。 字典 Redis 通常使用字典结构体存储用户散列数据。字典是 Redis 的重要数据结构。除了散…

AGM AG32 (MCU + FPGA)在音频处理上的应用案例

AGM AG32 (MCU FPGA)在音频处理&#xff0c;比如语音识别、降噪、声音增强等方面上的应用很多&#xff0c;结合AG32的特点&#xff0c;这里做一个简单的分享。 AGM AG32的架构是FPGA MCU&#xff0c;并且AG32与STM32的关键兼容&#xff0c;这个低成本的AG32器件&#xff0c;在…

MongoDB基础入门到深入(八)MongoDB整合SpringBoot、Chang Streams

文章目录 系列文章索引十五、MongoDB整合SpringBoot1、环境准备2、集合操作3、文档操作&#xff08;1&#xff09;相关注解&#xff08;2&#xff09;创建实体&#xff08;3&#xff09;添加文档&#xff08;4&#xff09;查询文档&#xff08;5&#xff09;更新文档&#xff0…

paligemma、Grounding-DINO-1.5简单无需标注无需训练直接可以使用的VLM图像到文本模型

1、paligemma 参考:https://github.com/google-research/big_vision/blob/main/big_vision/configs/proj/paligemma/README.md 模型架构: 文本与图像特征一起送入大模型 在线体验网址: https://huggingface.co/spaces/big-vision/paligemma 通过文字prompt既可与图片对话…

工行音视频服务平台建设与应用经验

近些年来&#xff0c;伴随着技术能力的积累突破&#xff0c;音视频服务开始蓬勃生长走进千家万户&#xff0c;使用远程视频通话、观看各类视频直播逐渐成为人们的日常&#xff0c;而金融服务作为社会生活的重要组成部分&#xff0c;自然需要积极拥抱应用新技术。 如今&#xff…

目标检测——无人机垃圾数据集

引言 亲爱的读者们&#xff0c;您是否在寻找某个特定的数据集&#xff0c;用于研究或项目实践&#xff1f;欢迎您在评论区留言&#xff0c;或者通过公众号私信告诉我&#xff0c;您想要的数据集的类型主题。小编会竭尽全力为您寻找&#xff0c;并在找到后第一时间与您分享。 …

【C++入门】—— C++入门 (下)_内联函数

前言&#xff1a;在了解完前面的C基础内容后&#xff0c;马上我们就要真正不如C的学习了&#xff0c;但在之前让我们最后了解最后一点点C入门知识&#xff01;来迟的520特别篇&#xff01; 本篇主要内容&#xff1a; 内联函数 auto关键字 范围for 指针空值nullptr C入门 1. 内联…

Science| 单体耦合纤维实现无芯片纺织电子(纤维器件/智能织物/柔性可穿戴电子)

东华大学Hongzhi Wang,Chengyi Hou和Qinghong Zhang团队在《Science》上发布了一篇题为“Single body-coupled fiber enables chipless textile electronics”的论文。论文内容如下: 一、 摘要 智能纺织品为将技术融入日常生活中提供了理想的平台。然而,目前的纺织电子系统…

飞凌嵌入式亮相上海CPSE,展现智能充储技术新力量

5月22日~24日&#xff0c;第三届上海国际充电桩及换电站展览会(CPSE)在上海汽车会展中心举行&#xff0c;飞凌嵌入式以“聚焦充电桩主控智造赋能车桩智联”为主题参展&#xff0c;与来自全国的客户朋友及行业伙伴一同交流分享&#xff0c;展位号Z15。 作为国内较早从事嵌入式技…

HTTPS:安全网络通信的基石

在数字化时代&#xff0c;网络通信的安全变得至关重要。HTTPS&#xff08;超文本传输安全协议&#xff09;是一种用于保护网络通信的协议&#xff0c;它通过加密技术确保数据传输的安全性和完整性。下面我们就来了解一下HTTPS。 一、HTTPS是什么&#xff1f; HTTPS是HTTP&…

罗德与施瓦茨FPS7频谱分析仪怎么判断真实信号?

频谱分析仪是电子测量领域的重要仪器&#xff0c;可以帮助工程师、研究人员分析信号的频域特性&#xff0c;为设备调试、故障诊断等提供有价值的数据支持。作为业界领先的频谱分析仪制造商&#xff0c;罗德与施瓦茨的FPS7型号在精度、灵敏度和分辨率等指标上都有出色表现&#…

《最新出炉》系列初窥篇-Python+Playwright自动化测试-39-highlight() 方法之追踪定位

1.简介 在之前的文章中宏哥讲解和分享了&#xff0c;为了看清自动化测试的步骤&#xff0c;通过JavaScript添加高亮颜色&#xff0c;就可以清楚的看到执行步骤了。在学习和实践Playwright的过程中&#xff0c;偶然发现了使用Playwright中的highlight()方法也突出显示Web元素。…

macOS Monterey 12.7.5 (21H1222) 正式版发布,ISO、IPSW、PKG 下载

macOS Monterey 12.7.5 (21H1222) 正式版发布&#xff0c;ISO、IPSW、PKG 下载 5 月 13 日凌晨&#xff0c;macOS Sonoma 14.5 发布&#xff0c;同时带来了 macOS Ventru 13.6.7 和 macOS Monterey 12.7.5 安全更新。 本站下载的 macOS 软件包&#xff0c;既可以拖拽到 Appli…