feat(errors): add centralized exception handling
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { AppException, ExceptionCode } from "@/core/errors";
|
||||
|
||||
import { Result, toError } from "../result";
|
||||
|
||||
describe("Result error normalization", () => {
|
||||
it("normalizes unknown thrown values to AppException", async () => {
|
||||
const result = await Result.wrap(async () => {
|
||||
throw "boom";
|
||||
});
|
||||
|
||||
expect(Result.isErr(result)).toBe(true);
|
||||
if (Result.isOk(result)) throw new Error("Expected Result.err");
|
||||
|
||||
expect(result.error).toBeInstanceOf(AppException);
|
||||
expect(result.error.message).toBe("boom");
|
||||
expect((result.error as AppException).code).toBe(ExceptionCode.unknown);
|
||||
});
|
||||
|
||||
it("returns AppException from toError", () => {
|
||||
const error = toError(new TypeError("Failed to fetch"));
|
||||
|
||||
expect(error).toBeInstanceOf(AppException);
|
||||
expect(error.message).toBe("Network connection failed.");
|
||||
expect((error as AppException).code).toBe(ExceptionCode.network);
|
||||
});
|
||||
});
|
||||
+15
-18
@@ -10,10 +10,13 @@
|
||||
* 如需在同模块中混用,可用 `import { Result as StorageResult } from
|
||||
* "@/data/storage/result"` 别名。
|
||||
*
|
||||
* `error` 固定为 `Error`(非 `unknown`):HTTP 拦截器与共享 unwrap 都抛 `ApiError`
|
||||
* (`ApiError extends Error`),仓库 `Result.wrap` 捕获后用 `toError` 规范化,
|
||||
* 调用方能可靠地 `instanceof ApiError` / `error.message` 访问错误信息。
|
||||
* `error` 仍固定为 `Error`(非 `unknown`):仓库 `Result.wrap` 捕获后用
|
||||
* `ExceptionHandler` 统一归一化为 `AppException`,调用方可以稳定读取
|
||||
* `error.message`;需要结构化错误时可继续通过 `ExceptionHandler.handle(error)`
|
||||
* 得到简化后的 `{ code, message }`。
|
||||
*/
|
||||
import { AppException, ExceptionHandler } from "@/core/errors";
|
||||
|
||||
import { Logger } from "./logger";
|
||||
|
||||
export type Result<T> =
|
||||
@@ -30,7 +33,7 @@ export const Result = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 构造失败结果。`error` 接受任意 thrown 值,内部用 `toError` 归一化为 `Error`。
|
||||
* 构造失败结果。`error` 接受任意 thrown 值,内部用 `toError` 归一化为 `AppException`。
|
||||
*/
|
||||
err<T = never>(error: unknown): Result<T> {
|
||||
return { success: false, error: toError(error) };
|
||||
@@ -56,11 +59,11 @@ export const Result = {
|
||||
* `*Api` 调用从「throw 风格」桥接到「Result 风格」。
|
||||
*
|
||||
* 错误处理策略(重点):
|
||||
* 1. 捕获后用 `toError` 规范化(保留 .stack / .code / .status 等字段)
|
||||
* 1. 捕获后用 `toError` 规范化为 `AppException`,原始 thrown 值放入 `cause`
|
||||
* 2. 用 Logger 记录(带 stack + component=Result 标签)—— 调试 dev 控制台可见,
|
||||
* 生产环境走 JSON pipeline 接入日志平台
|
||||
* 3. 不重新抛出 —— 调用方拿到的永远是 `Result<T>`(throw 风格调用方可在
|
||||
* 最外层显式 `throw Result.unwrap(r)`)
|
||||
* 最外层显式 `throw r.error`)
|
||||
*/
|
||||
async wrap<T>(thunk: () => Promise<T>): Promise<Result<T>> {
|
||||
try {
|
||||
@@ -77,17 +80,11 @@ export const Result = {
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 把任意 thrown 值规范化为 `Error`:
|
||||
* - 已是 `Error` 子类 → 原样返回(保留 `.code` / `.status` 等自定义字段)
|
||||
* - string → `new Error(str)`
|
||||
* - 其他 → `new Error(JSON.stringify(value))`,失败则 `new Error(String(value))`
|
||||
* 把任意 thrown 值规范化为 `AppException`:
|
||||
* - 已是 `AppException` → 原样返回
|
||||
* - 其他错误 → 交给 `ExceptionHandler` 收敛为 `{ code, message }`
|
||||
* - 原始值会保存在 `cause`,方便日志和调试继续追溯
|
||||
*/
|
||||
export function toError(value: unknown): Error {
|
||||
if (value instanceof Error) return value;
|
||||
if (typeof value === "string") return new Error(value);
|
||||
try {
|
||||
return new Error(JSON.stringify(value));
|
||||
} catch {
|
||||
return new Error(String(value));
|
||||
}
|
||||
export function toError(value: unknown): AppException {
|
||||
return ExceptionHandler.toError(value);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export class SessionAsyncUtil {
|
||||
const json = typeof value === "string" ? JSON.parse(value) : value;
|
||||
return Result.ok(schema.parse(json));
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export class SessionAsyncUtil {
|
||||
await SessionAsyncUtil._storage.setItem(key, JSON.stringify(validated));
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,15 +59,11 @@ export class SessionAsyncUtil {
|
||||
await SessionAsyncUtil._storage.removeItem(key);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toError(e: unknown): Error {
|
||||
return e instanceof Error ? e : new Error(String(e));
|
||||
}
|
||||
|
||||
function createDefaultSessionStorage(): Storage {
|
||||
if (typeof window === "undefined") {
|
||||
return createStorage({ driver: memoryDriver() });
|
||||
|
||||
+15
-22
@@ -50,7 +50,7 @@ export class SpAsyncUtil {
|
||||
if (value.length === 0) return Result.ok(null);
|
||||
return Result.ok(value);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export class SpAsyncUtil {
|
||||
await SpAsyncUtil._storage.setItem(key, value);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export class SpAsyncUtil {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return Result.ok(null);
|
||||
return Result.ok(Math.trunc(value));
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export class SpAsyncUtil {
|
||||
await SpAsyncUtil._storage.setItem(key, value);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export class SpAsyncUtil {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return Result.ok(null);
|
||||
return Result.ok(value);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ export class SpAsyncUtil {
|
||||
await SpAsyncUtil._storage.setItem(key, value);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export class SpAsyncUtil {
|
||||
if (typeof value !== "boolean") return Result.ok(null);
|
||||
return Result.ok(value);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export class SpAsyncUtil {
|
||||
await SpAsyncUtil._storage.setItem(key, value);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ export class SpAsyncUtil {
|
||||
if (!value.every((v) => typeof v === "string")) return Result.ok(null);
|
||||
return Result.ok(value);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ export class SpAsyncUtil {
|
||||
await SpAsyncUtil._storage.setItem(key, value);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ export class SpAsyncUtil {
|
||||
const parsed = schema.parse(json);
|
||||
return Result.ok(parsed);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ export class SpAsyncUtil {
|
||||
await SpAsyncUtil._storage.setItem(key, JSON.stringify(validated));
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ export class SpAsyncUtil {
|
||||
await SpAsyncUtil._storage.removeItem(key);
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ export class SpAsyncUtil {
|
||||
await SpAsyncUtil._storage.clear();
|
||||
return Result.ok(undefined);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,18 +208,11 @@ export class SpAsyncUtil {
|
||||
const exists = await SpAsyncUtil._storage.hasItem(key);
|
||||
return Result.ok(exists);
|
||||
} catch (e) {
|
||||
return Result.err(toError(e));
|
||||
return Result.err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 内部辅助
|
||||
// ============================================================
|
||||
function toError(e: unknown): Error {
|
||||
return e instanceof Error ? e : new Error(String(e));
|
||||
}
|
||||
|
||||
function createDefaultStorage(): Storage {
|
||||
if (typeof window === "undefined") {
|
||||
return createStorage({ driver: memoryDriver() });
|
||||
|
||||
Reference in New Issue
Block a user