Building Error Handling (feat. Java, Go, Rust, TS)

打造对开发者友好的错误处理方式(feat. Java, Go, Rust, TS)

前言

错误处理是所有程序都会遇到的问题,在复杂的 Web 开发中尤其如此。本文将结合不同的模式与语言,探讨如何「打造对开发者友好的错误处理方式」。推荐演讲: 意想不到的 Monad:能在 JS/TS 中实现安全的错误处理吗?由 Artem Kobzar 和 Dmitry Makhnev 主讲 - JSConf🔗 。

定义「错误」的差异: Exception 与 Error

  • Exception(异常):程序可以预期并处理的问题
    • 用户输入错误的账号密码
  • Error(错误):程序无法处理的严重问题
    • 系统崩溃、内存不足

JavaScript try catch 有什么不足之处?

大多数语言都有 try-catch 机制,看起来很直观,以 JavaScript 为例:

try {
doThings()
} catch(error) {
console.error(error)
}

但实际使用时会遇到以下问题:

  1. 静态类型分析:在弱类型语言中处理异常时,由于没有显式类型,因此必须进行类型检查或断言。
try {
throw new Error('错误');
} catch (error) {
// error 可能是任何东西,需要运行时检查
if (error instanceof Error) {
console.error(error.message);
}
}
  1. 佛系处理:没有强制要求处理错误或异常,容易被忽略。
  2. 异常错误难分:任何问题都会被捕获,而且不知道错误是在哪里抛出的,拆分成多个 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;
}

借鉴不同语言和生态的错误处理

Java 显式处理错误

在 Java 中,受检异常(Checked Exception)的处理是显式的,必须在代码里写出处理方式,否则连编译都无法通过。

class PositiveNumber {
private int value;
public PositiveNumber(int number) throws NegativeNumberException {
if (number <= 0) {
throw new NegativeNumberException("数字必须为正数: " + number);
}
this.value = number;
}
public int getValue() {
return value;
}
}
PositiveNumber num = new PositiveNumber(-5); // 编译失败,因为没有处理异常

Node.js 回调风格:将异常作为返回值

既然希望强制处理异常值,何不将异常作为执行结果返回?例如,早期 JavaScript Node.js 社区有一种惯例,使用 Error First Callback 处理异步代码(当时连 Promise🔗 或 Async Await🔗 都尚未推出)。通过回调函数传递结果,「错误」依旧可以通过抛出和捕获来处理,而「异常」则被视为需要显式处理的程序逻辑。

fs.readFile('foo.txt', (err, data) => {
if (err) {
// 处理错误
} else {
// 使用 data
}
})

但随着程序逻辑变得复杂,回调地狱🔗 不可避免,而且这种模式需要开发者之间的默契与配合。

Go 多返回值

Go 不采用 throw Error、try catch 这样的异常与错误处理流程,而是依赖「多返回值 Multiple Return Values🔗」这一语言特性,同样将异常作为值返回,这是 Go 社区常见的错误处理方式。

func main() {
file, err := os.Open("File.txt")
if err != nil {
log.Fatal(err)
}
fmt.Print(file)
}

也有人不喜欢 Go 缺少简单的异常抛出机制,所有错误都要依靠检查返回值来处理:

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

Rust Result 类型

相比 Go 通常通过多返回值返回结果与错误,Rust 通过 Result<T, E> 类型显式返回「成功或失败」,这种机制是强制的,并且内置于语言之中:

fn read_file() -> Result<String, std::io::Error> {
let content = std::fs::read_to_string("foo.txt")?;
Ok(content)
}
fn main() {
match read_file() {
Ok(data) => println!("内容: {}", data),
Err(e) => eprintln!("错误: {}", e),
}
}
  • Result<T, E>:一个枚举类型(Enum),只可能是 Ok(T) 或 Err(E),因此调用者必须显式处理。
  • ? 运算符:语法糖,遇到 Err 时会自动向上返回错误,让程序更简洁,但仍保持显式处理。

这里的 Result 类型体现了一种 Monad 模式:将计算过程包装成「盒子」,安全地操作其中的值,而不必担心或检查其中是否存在错误。

fn safe_divide(x: i32, y: i32) -> Result<i32, String> {
if y == 0 {
Err("除数为零".to_string())
} else {
Ok(x / y)
}
}
fn main() {
let result = Ok(2)
.map(|x| x + 4) // Ok(6)
.and_then(|x| safe_divide(x, 0)) // Err("除数为零")
.map(|x| x * 2) // 跳过(因为是 Err)
.map(|x| x - 1) // 跳过
.unwrap_or(-999); // 因为出错,返回默认值 -999
println!("{:?}", result); // -999
}

改善 JavaScript 错误处理体验

Result 类型

借鉴 Go 与 Rust「将错误作为返回值」的错误处理模式,通过 TypeScript 定义 Result 类型,搭配 ok 与 err 函数创建响应对象,就能快速实现类似的体验:

type Result<T> = { success: true; value: T } | { success: false; error: Error };
function ok<T>(value: T): Result<T> {
return { success: true, value };
}
function err(error: Error): Result<never> {
return { success: false, error };
}
function hello(name: string): Result<string> {
if (name === "") {
return err(new Error("姓名为空"));
}
return ok(`你好,${name}。欢迎!`);
}
function main(): void {
const result = hello('');
if (result.success) {
console.log(result.value)
} else {
console.log(result.error)
}
}

Maybe Monad

class Maybe {
constructor(value) {
this.value = value;
}
static just(value) {
return new Maybe(value);
}
static nothing() {
return new Maybe(null);
}
isNothing() {
return this.value === null || this.value === undefined;
}
map(fn) {
if (this.isNothing()) {
return Maybe.nothing();
}
return Maybe.just(fn(this.value));
}
flatMap(fn) {
if (this.isNothing()) {
return Maybe.nothing();
}
return fn(this.value);
}
getOrElse(defaultValue) {
if (this.isNothing()) {
return defaultValue;
}
return this.value;
}
}
const safeDivide = (x, y) => {
if (y === 0) {
return Maybe.nothing();
}
return Maybe.just(x / y);
};
const result1 = Maybe.just(2)
.map(x => x + 4)
.flatMap(x => safeDivide(x, 3))
.map(x => x * 2)
.map(x => x - 1)
.getOrElse("错误!");
console.log(result1); // 3
const result2 = Maybe.just(2)
.map(x => x + 4)
.flatMap(x => safeDivide(x, 0))
.map(x => x * 2)
.map(x => x - 1)
.getOrElse("错误!");
console.log(result2); // 错误!
class Maybe<T> {
private constructor(private readonly value: T | null) {}
static just<T>(value: T): Maybe<T> {
return new Maybe(value);
}
static nothing<T>(): Maybe<T> {
return new Maybe<T>(null);
}
isNothing(): boolean {
return this.value === null || this.value === undefined;
}
map<U>(fn: (value: T) => U): Maybe<U> {
if (this.isNothing()) {
return Maybe.nothing<U>();
}
return Maybe.just(fn(this.value!));
}
flatMap<U>(fn: (value: T) => Maybe<U>): Maybe<U> {
if (this.isNothing()) {
return Maybe.nothing<U>();
}
return fn(this.value!);
}
getOrElse(defaultValue: T): T {
if (this.isNothing()) {
return defaultValue;
}
return this.value!;
}
filter(predicate: (value: T) => boolean): Maybe<T> {
if (this.isNothing() || !predicate(this.value!)) {
return Maybe.nothing<T>();
}
return this;
}
toNullable(): T | null {
return this.value;
}
static fromNullable<T>(value: T | null | undefined): Maybe<T> {
return value != null ? Maybe.just(value) : Maybe.nothing<T>();
}
}
const safeDivide = (x: number, y: number): Maybe<number> => {
if (y === 0) {
return Maybe.nothing<number>();
}
return Maybe.just(x / y);
};
const result1 = Maybe.just(2)
.map(x => x + 4)
.flatMap(x => safeDivide(x, 3))
.map(x => x * 2)
.map(x => x - 1)
.getOrElse(-1);
console.log(result1); // 3
const result2 = Maybe.just(2)
.map(x => x + 4)
.flatMap(x => safeDivide(x, 0))
.map(x => x * 2)
.map(x => x - 1)
.getOrElse(-1);
console.log(result2); // -1
// 使用 fromNullable 的示例
const parseNumber = (str: string): Maybe<number> => {
const num = parseFloat(str);
return isNaN(num) ? Maybe.nothing<number>() : Maybe.just(num);
};
const calculation = parseNumber("10")
.flatMap(x => safeDivide(x, 2))
.map(x => x + 1)
.filter(x => x > 5)
.getOrElse(0);
console.log(calculation); // 6
// 处理可能为 null 的 API 响应
interface User {
id: number;
name: string;
email?: string;
}
const getUser = (): User | null => {
return { id: 1, name: "John", email: "[email protected]" };
};
const userEmail = Maybe.fromNullable(getUser())
.map(user => user.email)
.flatMap(email => Maybe.fromNullable(email))
.map(email => email.toLowerCase())
.getOrElse("[email protected]");
console.log(userEmail); // [email protected]

延伸阅读