ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TypeScript 异常处理实战指南:throw、Error 子类与“异常应属例外“的工程实践

TypeScript 异常处理实战指南:throw、Error 子类与“异常应属例外“的工程实践 TypeScript 异常处理实战指南throw、Error 子类与异常应属例外的工程实践【免费下载链接】typescript-book:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 项目地址: https://gitcode.com/gh_mirrors/ty/typescript-book本文是 TypeScript Deep Dive本仓库 README.md 所介绍的开源 TypeScript 深度教程类型系统章节中的异常处理专题。它以 JavaScript 原生异常机制为起点讲解throw与try/catch的用法、内置Error子类的语义、为何永远抛出Error对象而非裸字符串以及 Node.js 回调式错误传递约定最后深入探讨异常应属例外Exceptions should be exceptional这一工程原则在 TypeScript 类型系统中的落地方式。读完本文你将掌握一套既能正确捕获运行时错误、又能在类型系统中显式表达失败路径的异常处理方案。一、基础throw与try/catchJavaScript 内置了Error类专门用于表达异常。抛出一个错误使用throw关键字捕获它则使用try/catch语句对try { throw new Error(Something bad happened); } catch(e) { console.log(e); }这段代码的执行流程是try块内的throw语句会中断当前执行流将Error实例向上抛出catch块随即捕获该实例并交给变量e。TypeScript 完全继承这一运行时语义——异常机制发生在 JavaScript 运行时类型系统并不会拦截throw因此这套写法在.ts文件中同样成立。二、内置 Error 子类型除了基类Error之外JavaScript 运行时还内置了一批继承自Error的错误类在特定场景下由运行时自动抛出。理解它们的触发条件能帮助你在日志与堆栈中快速定位问题根源RangeError当数值变量或参数超出其合法范围时抛出// Call console with too many arguments console.log.apply(console, new Array(1000000000)); // RangeError: Invalid array length上例中new Array(1000000000)请求创建一个长度过大的数组超出引擎允许范围于是运行时抛出RangeError。ReferenceError当解引用一个无效引用即访问未定义的变量时抛出use strict; console.log(notValidVar); // ReferenceError: notValidVar is not defined注意示例开头启用了严格模式use strict。在非严格模式下访问未声明变量通常只会静默地得到undefined或产生隐式全局变量而严格模式会直接以ReferenceError暴露这一错误。SyntaxError解析不符合 JavaScript 语法的代码时抛出1***3; // SyntaxError: Unexpected token *1***3不是合法的表达式解析阶段即失败抛出SyntaxError。TypeError当变量或参数的类型不符合预期时抛出(1.2).toPrecision(1); // TypeError: 1.2.toPrecision is not a function字符串类型并不具备toPrecision方法调用一个不存在的函数即触发TypeError。这类错误在 TypeScript 中尤其值得注意类型系统能在编译期拦截大部分在错误类型上调用方法的写法例如本仓库 code/types/types.ts 演示的num 123、str 123等赋值错误。URIError当encodeURI()或decodeURI()收到非法参数时抛出decodeURI(%); // URIError: URI malformed%不是合法的 URI 编码序列decodeURI无法解析于是抛出URIError。这一组子类的存在意义在于它们不仅携带错误信息还通过类名区分了错误语义。TypeScript 侧的全局声明同样体现了这套体系——本仓库 code/compiler/typings/node/node.d.ts 中列出了Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError等构造函数的类型声明你可以在类型安全的代码中直接catch并按类型区分处理。三、永远抛出Error对象而非裸字符串初学者有时会直接抛出字符串try { throw Something bad happened; } catch(e) { console.log(e); }不要这样做。Error对象的根本价值在于它通过stack属性自动记录对象被创建与抛出的原始位置文件、行号、调用栈。裸字符串则完全丢失了这些上下文调试时无法得知错误来自哪一行代码在日志聚合、错误上报等场景下裸字符串会让问题分析变得极其痛苦。因此无论何时需要表达失败都应构造Error实例或其子类再抛出。这也是日志可观测性的第一道保障。四、不一定非要throw回调风格的错误传递异常不一定只能通过throw/catch传播。直接传递Error对象同样合法这正是 Node.js 回调风格代码的惯例——回调函数的第一个参数约定为错误对象function myFunction (callback: (e?: Error)) { doSomethingAsync(function () { if (somethingWrong) { callback(new Error(This is my error)) } else { callback(); } }); }这里回调签名(e?: Error)表示错误参数可选一切正常时调用callback()不传参数出错时调用callback(new Error(...))。调用方据此判断是否发生了错误。这种错误优先回调error-first callback约定在本仓库的类型示例中也能找到印证code/types/type-compatibility.ts 中定义了一个接收(err: Error, data: any) void回调的函数并演示了参数个数兼容性的边界——回调函数可以少声明参数如(err) null但不能多声明参数(err, data, more) null会报错因为调用方可能不会传入多余的参数。此外Node.js 类型声明中的ErrnoException extends Error见 code/compiler/typings/node/node.d.ts为这类回调的err参数补充了errno、code、path、syscall等字段进一步说明错误作为普通对象流转是 Node 生态的标准做法。五、异常应属例外为什么不要滥用 throw异常应属例外Exceptions should be exceptional是计算机科学中的常见说法。在 JavaScript 与 TypeScript 中同样成立原因有三。1. 无法得知异常从哪里抛出看下面这段代码try { const foo runTask1(); const bar runTask2(); } catch(e) { console.log(Error:, e); }下一位维护者无法判断究竟是哪个函数抛出了错误审查代码的人必须深入runTask1/runTask2及其调用的所有函数的实现才能确认。异常传播路径是隐式的这严重损害了代码的可读性与可维护性。2. 让优雅降级变得困难如果为每个可能抛错的操作单独包一层try/catch代码会迅速变得笨重try { const foo runTask1(); } catch(e) { console.log(Error:, e); } try { const bar runTask2(); } catch(e) { console.log(Error:, e); }更糟的是当第二个任务需要依赖第一个任务的结果时代码会进一步退化——foo必须先以let声明并显式标注类型因为其初值无法从runTask1的返回类型推断let foo: number; // Notice use of let and explicit type annotation try { foo runTask1(); } catch(e) { console.log(Error:, e); } try { const bar runTask2(foo); } catch(e) { console.log(Error:, e); }可见为了防御性地包裹异常我们被迫放弃const与类型推断代码的可读性和类型安全双双受损。3. 异常无法在类型系统中被表达这是最关键的一点。考虑如下校验函数function validate(value: number) { if (value 0 || value 100) throw new Error(Invalid value); }用throw表达失败是个坏主意因为抛出的可能性完全没有体现在该函数的类型定义中——它的类型仍是(value: number) void调用方从签名上根本看不出它会失败。更好的做法是把失败显式编码进返回类型function validate(value: number): {error?: string} { if (value 0 || value 100) return {error:Invalid value}; }现在失败路径以及成功路径都被类型系统如实记录了返回值要么是{ error: Invalid value }要么是{}可选属性error缺省。调用方必须处理这种可能性编译器也会强制他们面对这一点。除非你想以非常泛化的方式简单、catch-all 等处理错误否则不要throw错误。这条原则可总结为能用返回值表达失败就不要用异常。异常适用于确实例外的场景如运行时环境异常、不可恢复的错误而可预期的业务性失败如参数校验不通过应通过类型系统显式建模。六、与类型系统的交汇never与穷尽检查异常处理与 TypeScript 类型系统还有一个重要交汇点永远抛错的函数。当函数的代码路径必然抛出异常时其返回类型在类型层面被表示为neverbottom type。例如function foo(x: string | number): boolean { if (typeof x string) { return true; } else if (typeof x number) { return false; } // TypeScript 理解 fail 返回 never因此这里不会报不是所有代码路径都有返回值 return fail(Unexhaustive!); } function fail(message: string): never { throw new Error(message); }这类用never做穷尽性检查的完整机制参见本仓库的 docs/types/never.md它与联合类型判别discriminated union结合可用于编译期穷尽检查见 docs/types/discriminated-unions.md。在阅读本篇时你可以将这些内容视为异常在类型系统边缘的延伸异常虽然默认不进入类型签名但 TypeScript 通过never为必然抛错的路径提供了类型级表达。七、工程实践小结结合本篇内容与仓库中的配套示例推荐如下异常处理策略场景推荐做法依据表达失败且失败可预期用返回值如{error?: string}显式建模本文第五节异常无法被类型系统表达表达失败且属于真正例外throw new Error(...)并尽量使用语义化的子类本文第二节、第三节Node.js 回调风格代码遵循 error-first 约定直接传递Error对象本文第四节code/types/type-compatibility.ts必然抛错的辅助函数显式标注返回类型never用于穷尽检查docs/types/never.md在类型安全方面TypeScript 的strict系列选项如 docs/options/strictNullChecks.md 讨论的strictNullChecks能进一步约束undefined与null的传递让返回值表达失败的模式更加可靠。若想在真实项目中验证这些错误捕获与处理机制本仓库 code/compiler/scanner/runScanner.ts 展示了一个贴近底层的例子TypeScript 编译器自身的 scanner 通过setOnError注册错误回调以事件驱动方式收集扫描阶段的诊断信息——这正是不滥用 throw、让错误沿显式通道流动思想的体现。【免费下载链接】typescript-book:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 项目地址: https://gitcode.com/gh_mirrors/ty/typescript-book创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表