前言
错误处理是所有程序都会遇到的问题,在复杂的 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)}但实际使用时会遇到以下问题:
- 静态类型分析:在弱类型语言中处理异常时,由于没有显式类型,因此必须进行类型检查或断言。
try { throw new Error('错误');} catch (error) { // error 可能是任何东西,需要运行时检查 if (error instanceof Error) { console.error(error.message); }}- 佛系处理:没有强制要求处理错误或异常,容易被忽略。
- 异常错误难分:任何问题都会被捕获,而且不知道错误是在哪里抛出的,拆分成多个 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 => {};
const userEmail = Maybe.fromNullable(getUser()) .map(user => user.email) .flatMap(email => Maybe.fromNullable(email)) .map(email => email.toLowerCase())
延伸阅读
- 使用 JavaScript try…catch 控制程序中的错误 - WebDong
- 如何处理 TypeScript 抛出的错误? - WebDong
- JavaScript 中的 Go 风格错误处理模式 - 5 Error
- 图解 Monad - 阮一峰的网络日志
- Monad 其实很简单(攻克函数式编程的最终 Boss)
- 我改进了 JavaScript 的错误处理 - Bret Cameron