在 JavaScript 开发中,通常都不太重视起错误处理,捕获和记录错误对于任何项目的开发周期都是至关重要的。随着 TypeScript 项目开发多了,开始意识到并不真正了解错误处理。经常在项目代码中看到一下类似代码:
try { throw new Error("Oops") } catch (error) { console.error(error.message) }
error
是 unknown
类型 ,因此在将其转换为新类型或缩小类型范围之前,不能对 error
执行任何操作。正确的处理方式是缩小类型,将看看如何做到这一点,但为什么这是必要的?
在 JavaScript 中,几乎任何东西都可以被抛出:
throw "oops" throw 210 throw null throw { message: "异常错误" }
所以真正被捕获的错误是未知的。但是,可以通过使用 TypeScript 的多种方式干净地处理错误。
JavaScript错误的基础知识
JavaScript 中的错误类型,在 JavaScript 中有许多类型的错误,但最常见的是:
ReferenceError
:代码引用了一个不存在的变量。TypeError
:值不是预期的错误类型SyntaxError
:代码在语法上无效
抛出错误
有时需要手动抛出错误,例如,可能有一些代码依赖于函数调用的返回值,但有可能该值是 undefined
,或者至少在 TypeScript 认为是 undefined
。在下面这个例子中,抛出是缩小返回用户范围的最佳解决方案。
// 通常方式 function createProject() { const user = getUser(); saveProject({ name: "", userId: user.id }) } // 避免异常 function createProject() { const user = getUser(); if (!user) { return; } saveProject({ name: "", userId: user.id }) } // 最佳方式,抛出异常 function createProject() { const user = getUser(); if (!user) { throw new ReferenceError('用户不存在') } saveProject({ name: "", userId: user.id }) }
捕获错误
一旦错误被抛出,它将在调用堆栈中冒泡,直到被 try/catch
语句捕获。当在 try
块内运行的代码抛出错误时,它将在 catch
块中被捕获,错误可能源自嵌套在函数内部的函数,并且会冒泡直到被捕获。
try { throw new ReferenceError(); } catch (error) { console.error(error) }
缩小错误类型
一旦被捕获,检查所抛出的错误类型可能很有用。这使能够将类型从未知缩小到可以与之交互的特定类型(可以直观的理解错误),可以用 instanceof
做到这一点:
try { throw new ReferenceError(); } catch (error) { if (error instanceof ReferenceError) { console.error(error.message) } }
设计模式
设计模式是软件设计中常见问题的解决方案,这些模式很容易重复使用并且富有表现力。在最新的项目中,将代码按域分组在名为 Features
的目录中,它可以包含相关的组件、钩子、类型、错误等等,每个 Feature
目录都包含一个 errors.ts
文件,在其中为各自的域定义了一个自定义错误类。
创建自定义错误类型
在 errors.ts
文件中,导出了一个 class
。为潜在名称维护一个联合类型,这增加了一些不错的智能感知和类型安全。该类扩展了 Error 对象,它允许插入堆栈跟踪(对于大多数 JS 运行时)。
type ErrorName = 'GET_PROJECT_ERROR' | 'CREATE_PROJECT_ERROR' | 'PROJECT_LIMIT_REACHED'; export class ProjectError extends Error { name: ErrorName; message: string; cause: any; constructor({ name, message, cause }: { name: ErrorName; message: string; cause?: any; }) { super(); this.name = name; this.message = message; this.cause = cause; } }
抛出自定义错误
实例化新错误时,name
值具有智能感知,并且必须是联合类型中定义的名称之一。
export async function createProject() { const { data, error } = await api.createProject(); if (error) { throw new ProjectError({ name: "CREATE_PROJECT_ERROR", message: "API error occurred while creating project", cause: error }) } if (data.length === projectLimit) { throw new ProjectError({ name: "PROJECT_LIMIT_REACHED", message: "Project limit has been reached." }) } return data; }
捕获自定义错误
当错误被捕获时,可以使用 instanceof
缩小错误类型。一旦缩小范围,error.name
就会智能感知,此时可以根据抛出的错误名称执行逻辑。在此示例中,PROJECT_LIMIT_REACHED
错误是要向用户显示的错误,提供了一条专门为用户呈现的消息。
try { await createProject(); } catch (error) { if (error instanceof ProjectError) { if (error.name === "PROJECT_LIMIT_REACHED") { toast(error.message) } } }
定义可重用的错误库
由于项目中有很多 errors.ts
文件,类中唯一的动态代码是名称的联合类型,因此可以对代码进行优化,创建了一个 ErrorBase
类,它接受用作名称类型的泛型。
export class ErrorBase<T extends string> extends Error { name: T; message: string; cause: any; constructor({ name, message, cause }: { name: T, message: string, cause?: any }) { super(); this.name = name; this.message = message; this.cause = cause } }
现在,当创建一个新的自定义错误类时,可以扩展这个基类,需要做的就是给它提供可用名称的联合类型。
import { ErrorBase } from "./error-base" type ErrorName = 'GET_PROJECT_ERROR' | 'CREATE_PROJECT_ERROR' | 'PROJECT_LIMIT_REACHED'; export class TeamError extends ErrorBase<ErrorName>{ }
总结
设计模式让代码变得更容易维护,处理错误只是维护良好的应用程序的一部分,另一个重要步骤是使用类似 Sentry 的工具记录跟踪错误。