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

在这里插入图片描述

原文: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 做一些事情

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Go微服务——go-micro v4安装使用

安装go-micro 打开cmd窗口&#xff0c;执行以下命令 go install github.com/go-micro/cli/cmd/go-microlatest测试是否成功安装 go-micro -v创建服务 go-micro new service helloworldwindows 安装make 安装地址 https://gnuwin32.sourceforge.net/packages/make.htm 配置…

python从0开始学习(九)

前言 上一篇文章我们介绍了python中的序列类型和元组类型&#xff0c;本篇文章将接着往下将。 1、字典类型 字典类型是根据一个信息查找另一个信息的方式所构成的“键值对”&#xff0c;它表示索引用的键和对应的值构成的成对关系。它是一个可变数据类型&#xff0c;也就是说它…

JAVA基础知识100题练习、蓝桥杯竞赛题,编程基础必练题!

各位编程小伙伴们&#xff0c;这里可是作者花费了无数个日日夜夜&#xff0c;熬秃了不知道多少根头发&#xff0c;凭借着那超级无敌多年的编程经验&#xff0c;拼死拼活、千辛万苦总结出来的呀&#xff01;这可是各种开发语言都绝对必须要练的基础编程知识哇&#xff01;什么九…

jenkins自动化部署详解

一、准备相关软件 整个自动化部署的过程就是从git仓库拉取最新代码&#xff0c;然后使用maven进行构建代码&#xff0c;构建包构建好了之后&#xff0c;通过ssh发送到发布服务的linux服务器的目录&#xff0c;最后在此服务器上执行相关的linux命令进行发布。 此篇文章jenkins…

Linux gurb2简介

文章目录 前言一、GRUB 2简介二、GRUB 2相关文件/文件夹2.1 /etc/default/grub文件2.2 /etc/grub.d/文件夹2.3 /boot/grub/grub.cfg文件 三、grubx64.efi参考资料 前言 简单来说&#xff0c;引导加载程序&#xff08;boot loader&#xff09;是计算机启动时运行的第一个软件程…

262 基于matlab的一级倒立摆仿真

基于matlab的一级倒立摆仿真&#xff0c;在对一级倒立摆进行数学建模的基础上&#xff0c;对模型进行线性化&#xff0c;得到其状态空间模型&#xff0c;利用二次型最优控制方法得出控制率。输出角度和位置优化曲线。程序已调通&#xff0c;可直接运行。 262 一级倒立摆仿真 状…

Sui生态DeFi项目Cetus和Aftermath宣布启动孵化器

Sui DeFi中的去中心化交易所Cetus和Aftermath Finance联合Sui基金会宣布启动新的孵化器&#xff0c;为初创项目提供更多可行性途径。这两个DeFi项目在Sui上有着较长的历史&#xff0c;自去年一同与主网推出以来&#xff0c;目前在TVL方面位居前五。这两个项目的持久性和成功使它…

构建品牌长期价值:海外KOC营销的持续性策略解析

在当今数字化时代&#xff0c;随着社交媒体的兴起&#xff0c;消费者对于品牌的信任和认可越来越依赖于个人的推荐和体验。因此&#xff0c;KOC营销成为了品牌推广中的重要策略之一。但是&#xff0c;要想构建品牌的长期价值&#xff0c;单纯地依靠一次性的KOC合作是远远不够的…

【云原生】K8s管理工具--Kubectl详解(一)

一、陈述式管理 1.1、陈述式资源管理方法 kubernetes 集群管理集群资源的唯一入口是通过相应的方法调用 apiserver 的接口kubectl 是官方的 CLI 命令行工具&#xff0c;用于与 apiserver 进行通信&#xff0c;将用户在命令行输入的命令&#xff0c;组织并转化为apiserver 能识…

深度学习之基于Matlab的BP神经网络交通标志识别

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景与意义 随着智能交通系统&#xff08;ITS&#xff09;的快速发展&#xff0c;交通标志识别&#xff0…

十年磨一剑“2024成都电子信息展会”推动电子产业全球发展

2024成都电子展&#xff0c;招商工作已接近尾声&#xff0c;这场盛大的展会不仅是电子信息行业的一次盛会&#xff0c;更是中国西部电子信息产业发展的重要里程碑。自2013年起&#xff0c;中国&#xff08;西部&#xff09;电子信息博览会便选择成都作为其永久的举办地&#xf…

【誉天618·年中钜惠】无忧卡计算VIP会员上线

在数字化浪潮汹涌的当下&#xff0c;技术更新迭代的速度令人目不暇接。为了满足广大技术爱好者与专业人士对于最新技术的渴望与追求。2022年我们推出了誉天系列会员卡&#xff0c;受到了学员和企业的一致好评&#xff0c;现在我们又结合当下的热门ICT技术进行了会员卡升级。 誉…

spring和springboot、springcloud版本关系

进入新公司&#xff0c;看底层框架代码时&#xff0c;想了解spring的版本&#xff0c;很好奇这些版本之间时怎么对应的&#xff0c;因为不同版本应该有依赖关系&#xff0c;用得不对可能代码会有隐藏问题。 Spring、SpringBoot和SpringCloud的版本不一致&#xff0c;并且需要搭…

【Python】 如何对对象列表进行排序,有点意思

在Python中&#xff0c;我们经常需要对对象列表进行排序&#xff0c;这可以通过多种方式实现。当对象是一个自定义类实例时&#xff0c;排序通常基于对象的一个或多个属性。Python提供了内置的sorted()函数和列表的sort()方法&#xff0c;它们都允许我们指定一个排序的关键字。…

一次搞懂常见Banner尺寸,像素标准全解析!

在现代数字营销中&#xff0c;横幅banner广告是一种常见的形式&#xff0c;也是许多网站、博客和在线广告平台上常见的广告类型。然而&#xff0c;正确的横幅banner尺寸是至关重要的&#xff0c;因为它可以影响广告的可见性和效果。在本文中&#xff0c;我们将探讨横幅banner尺…

揭秘:APP广告变现逻辑

APP广告变现项目逻辑 一、项目背景与目标 在当今数字化时代&#xff0c;移动应用&#xff08;APP&#xff09;已成为人们日常生活中不可或缺的一部分。随着智能手机的普及&#xff0c;APP市场呈现出爆炸性增长&#xff0c;为开发者提供了巨大的商业机会。然而&#xff0c;如何在…

线程池(C++)

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 线程池 实现线程类 #pragma once#include <pthread.h> #include <iostream> #include <vector> #include <string> #include <cstdlib> #include <cstring> #include <functional&…

Transformer系列专题(一)——self-attention自注意力机制如何计算

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、总述二、如何计算self-attention三、softmax归一处理Attention整体计算流程 前言 自注意力机制的原理相当于一个词在其上下文中的联系&#xff0c;即将这一…

linux-----环境变量

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 常见的环境变量&#xff1a; PWD&#xff1a;记录当前工作目录 PATH : 指定命令的搜索路径 HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的…

毫米波雷达深度学习方法和技术-概述

深度学习方法和技术—在毫米波雷达解决方案的进展 前言 雷达已经从复杂的高端军事技术演变为相对简单的低端解决方案&#xff0c;渗透到工业、汽车和消费市场领域。这种快速发展是由硅的进步和使用深度学习算法来利用传感器数据的全部潜力所驱动的。雷达传感器的使用已经增长了…