错误码

在C语言中,错误处理一般依赖于返回错误码的方式。比如打开文件操作,失败时返回NULL,通过设置全局变量errno来指示具体的错误类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<errno.h>

int main() {
FILE * file = fopen("data.txt", "r");
if (file == NULL) {
perror("Error opening file");
} else {
int c;
while ((c = fgetc(file)) != EOF) {
putchar(c);
}
fclose(file);
}

return 0;
}
1
Error opening file: No such file or directory

这样的话,需要开发者在每次函数调用后检查返回的错误码来判断。

但是NULL 指针原本并不是表示错误的,不看文档,并不会知道NULL表示错误。

异常

现代编程语言中还有一些使用异常来进行错误处理。通过throw抛出一个自定义的异常对象;使用try...catch来捕获并处理可能抛出的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero is not allowed');
}
return a / b;
}

try {
let result = divide(10, 0);
console.log('Result:', result);
} catch (error) {
console.error('An error occurred:', error.message);
}

相比于错误码,显然自定义异常可读性更强。并且代码结构更加清晰,错误处理的代码与正常代码分隔开。也就是说,错误的产生和错误的处理分离。

多线程异常

异常在多线程环境下会有些棘手,以JavaScript为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// worker.js
const { parentPort } = require('worker_threads');

function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero is not allowed');
}
return a / b;
}

parentPort.on('message', () => {
let result = divide(10, 2);
parentPort.postMessage(result);
})

parentPort.on('message', () => {
let result = divide(10, 0);
parentPort.postMessage(result);
})
// main.js
const { Worker } = require('worker_threads');
const worker = new Worker('./worker.js');

worker.on('message', (msg) => {
try {
console.log(msg);
console.log(200);
} catch (err) {
console.log('main thread' + err);
}
});

worker.postMessage('start');
1
2
3
4
5
6
5
200
Error: Division by zero is not allowed
at divide (worker.js)
Emitted 'error' event on Worker instance at:
at [kOnErrorMessage] (node:internal/worker:300:10)

执行之后,发现及时报错,终止了程序,但是异常是主线程并没有捕获到异常。

加上以下代码后,发现不再报错了

1
2
3
// main.js
// ...
worker.on('error', (err) => {})

观察命令行中也有Emitted 'error' event,这是因为Node.js中线程间异常是通过触发error事件。但是这个异常处理并不是try...catch的形式。对于异步运行的代码,try...catch还是有局限的。

类型系统

Rust中使用Result类型来处理错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::fs::File;
use std::io;
use std::io::Read;

fn read_file_contents(file_path: &str) -> Result<String, io::Error> {
let mut file = File::open(file_path)?;

let mut contents = String::new();
file.read_to_string(&mut contents)?;

Ok(contents)
}

fn main() {
let file_path = "example.txt";
match read_file_contents(file_path) {
Ok(contents) => {
println!("File contents: {}", contents);
}
Err(error) => {
eprintln!("An error occurred: {}", error);
}
}
}

可以看到main中对Result类型的模式匹配。可能返回字符串,也可能是一个I/O错误。

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

对于多线程情况下的错误捕捉,Rust也是游刃有余

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use std::thread;
use std::sync::mpsc;

fn main() {
// 创建一个通道,用于主线程和子线程之间的通信
let (sender, receiver) = mpsc::channel();

// 在子线程中执行可能会出错的操作
thread::spawn(move || {
let result = do_something_that_may_fail();
sender.send(result).unwrap();
});

// 主线程接收子线程发送的消息
if let Ok(result) = receiver.recv() {
match result {
Ok(value) => {
println!("Operation successful: {}", value);
}
Err(error) => {
eprintln!("An error occurred in the thread: {}", error);
}
}
}
}

fn do_something_that_may_fail() -> Result<i32, &'static str> {
// 模拟一个可能会出错的操作
if false {
Ok(42)
} else {
Err("Operation failed")
}
}

而对于错误的传播,Rust中使用?向调用者返回错误,更加方便,不过其实也是模式匹配的语法糖。

结语

本文从错误码、异常、类型系统的方式展示了错误处理的不同方式,从错误码的低可读性和地位户型,到异常处理的自定义,但是多线程情况下错误捕捉受限,最后看到类型系统对错误处理的统一。Typescript的强类型系统自然也支持类型式的错误处理

参考

错误处理 - Rust 程序设计语言 中文版

错误处理:为什么Rust的错误处理与众不同?

Type-Safe Error Handling In TypeScript - DEV Community