name: 错误处理 description: 使用wellcrafted的trySync和tryAsync的错误处理模式。当编写错误处理代码、使用try-catch块或处理Result类型和优雅错误恢复时使用。 metadata: author: epicenter version: ‘1.1’
使用wellcrafted的trySync和tryAsync进行错误处理
使用trySync/tryAsync替代try-catch进行优雅错误处理
当处理可以优雅恢复的错误时,使用wellcrafted的trySync(用于同步代码)或tryAsync(用于异步代码)替代传统的try-catch块。这提供了更好的类型安全性和显式错误处理。
相关技能:参见
services-layer技能了解createTaggedError模式。参见query-layer技能了解错误转换为WhisperingError。
模式
import { trySync, tryAsync, Ok, Err } from 'wellcrafted/result';
// 同步:使用trySync进行同步操作
const { data, error } = trySync({
try: () => {
const parsed = JSON.parse(jsonString);
return validateData(parsed); // 自动包装在Ok()中
},
catch: (e) => {
// 优雅处理解析/验证错误
console.log('使用默认配置');
return Ok(defaultConfig); // 返回Ok并带有回退
},
});
// 异步:使用tryAsync进行异步操作
await tryAsync({
try: async () => {
const child = new Child(session.pid);
await child.kill();
console.log(`进程成功杀死`);
},
catch: (e) => {
// 优雅处理错误
console.log(`进程已经终止`);
return Ok(undefined); // 对于void函数返回Ok(undefined)
},
});
// 两者都支持相同的catch模式
const syncResult = trySync({
try: () => riskyOperation(),
catch: (error) => {
// 对于可恢复错误,返回带有回退值的Ok
return Ok('fallback-value');
// 对于不可恢复错误,返回Err
return ServiceErr({
message: '操作失败',
cause: error,
});
},
});
关键规则
- 选择正确的函数 - 同步代码使用
trySync,异步代码使用tryAsync - 总是await tryAsync - 与try-catch不同,tryAsync返回Promise,必须被await
- trySync立即返回 - 同步操作不需要await
- 匹配返回类型 - 如果try块返回
T,catch应返回Ok<T>进行优雅处理 - 使用Ok(undefined)表示void - 当函数返回void时,在catch中使用
Ok(undefined) - 返回Err进行传播 - 使用返回
Err的自定义错误构造函数,当您想要传播错误时 - 关键:使用Err()包装解构的错误 - 当您从tryAsync/trySync解构
{ data, error }时,error变量是原始错误值,不是包装在Err中的。您必须在返回前包装它:
// 错误 - error只是原始的TaggedError,不是Result
const { data, error } = await tryAsync({...});
if (error) return error; // 类型错误:返回TaggedError,不是Result
// 正确 - 使用Err()包装以返回正确的Result
const { data, error } = await tryAsync({...});
if (error) return Err(error); // 返回Err<TaggedError>
这与返回整个结果对象不同:
// 这也正确 - userResult已经是Result类型
const userResult = await tryAsync({...});
if (userResult.error) return userResult; // 返回完整的Result
示例
// 同步:JSON解析与回退
const { data: config } = trySync({
try: () => JSON.parse(configString),
catch: (e) => {
console.log('无效配置,使用默认值');
return Ok({ theme: 'dark', autoSave: true });
},
});
// 同步:文件系统检查
const { data: exists } = trySync({
try: () => fs.existsSync(path),
catch: () => Ok(false), // 假设检查失败时不存在
});
// 异步:优雅进程终止
await tryAsync({
try: async () => {
await process.kill();
},
catch: (e) => {
console.log('进程已经死亡,继续...');
return Ok(undefined);
},
});
// 异步:文件操作与回退
const { data: content } = await tryAsync({
try: () => readFile(path),
catch: (e) => {
console.log('文件未找到,使用默认内容');
return Ok('默认内容');
},
});
// 两者:错误传播(两者都适用)
const { data, error } = await tryAsync({
try: () => criticalOperation(),
catch: (error) =>
ServiceErr({
message: '关键操作失败',
cause: error,
}),
});
if (error) return Err(error);
何时使用trySync、tryAsync和try-catch
-
使用trySync时:
- 处理同步操作(JSON解析、验证、计算)
- 需要立即的Result类型而无需promises
- 在同步实用函数中处理错误
- 处理文件系统同步操作
-
使用tryAsync时:
- 处理async/await操作
- 发起网络请求或数据库调用
- 异步读写文件
- 任何返回Promise的操作
-
使用传统的try-catch时:
- 在模块级初始化代码中,不能使用await时
- 用于简单的即发即忘操作
- 当您处于函数上下文之外时
- 当与期望抛出异常的代码集成时
包装模式:最小化与扩展化
最小化包装原则
只包装可能失败的特定操作。 这样可以精确捕获错误边界,使代码更易于理解。
// ✅ 好:只包装有风险的操作
const { data: stream, error: streamError } = await tryAsync({
try: () => navigator.mediaDevices.getUserMedia({ audio: true }),
catch: (error) =>
DeviceStreamServiceErr({
message: `麦克风访问失败:${extractErrorMessage(error)}`,
}),
});
if (streamError) return Err(streamError);
// 继续执行非抛出操作
const mediaRecorder = new MediaRecorder(stream);
mediaRecorder.start();
// ❌ 坏:包装过多代码
const { data, error } = await tryAsync({
try: async () => {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream);
mediaRecorder.start();
await someOtherAsyncCall();
return processResults();
},
catch: (error) => GenericErr({ message: '某些操作失败' }), // 太模糊了!
});
立即返回模式
检查后立即返回错误。 这样可以创建清晰的控制流,防止错误嵌套。
// ✅ 好:立即检查并返回
const { data: devices, error: enumerateError } = await enumerateDevices();
if (enumerateError) return Err(enumerateError);
const { data: stream, error: streamError } = await getStreamForDevice(
devices[0],
);
if (streamError) return Err(streamError);
// 顺利继续快乐路径
return Ok(stream);
// ❌ 坏:嵌套错误处理
const { data: devices, error: enumerateError } = await enumerateDevices();
if (!enumerateError) {
const { data: stream, error: streamError } = await getStreamForDevice(
devices[0],
);
if (!streamError) {
return Ok(stream);
} else {
return Err(streamError);
}
} else {
return Err(enumerateError);
}
何时扩展try块
有时将多个操作放在一个try块中是合理的:
- 原子操作 - 当操作必须一起成功或失败时
- 相同错误类型 - 当所有操作产生相同错误类别时
- 清理逻辑 - 当任何失败时都需要清理时
// 扩展块在此处合适 - 所有操作都是“开始录制”的一部分
const { data: mediaRecorder, error: recorderError } = trySync({
try: () => {
const recorder = new MediaRecorder(stream, { bitsPerSecond: bitrate });
recorder.addEventListener('dataavailable', handleData);
recorder.start(TIMESLICE_MS);
return recorder;
},
catch: (error) =>
RecorderServiceErr({
message: `初始化录制器失败:${extractErrorMessage(error)}`,
}),
});
来自代码库的真实示例
最小化包装并立即返回:
// 来自device-stream.ts
async function getStreamForDeviceIdentifier(
deviceIdentifier: DeviceIdentifier,
) {
return tryAsync({
try: async () => {
const stream = await navigator.mediaDevices.getUserMedia({
audio: { ...constraints, deviceId: { exact: deviceIdentifier } },
});
return stream;
},
catch: (error) =>
DeviceStreamServiceErr({
message: `无法连接到麦克风。${extractErrorMessage(error)}`,
}),
});
}
多个最小化包装并立即返回:
// 来自navigator.ts
startRecording: async (params, { sendStatus }) => {
if (activeRecording) {
return RecorderServiceErr({ message: '已经在录制。' });
}
// 第一个try块 - 获取流
const { data: streamResult, error: acquireStreamError } =
await getRecordingStream({ selectedDeviceId, sendStatus });
if (acquireStreamError) return Err(acquireStreamError);
const { stream, deviceOutcome } = streamResult;
// 第二个try块 - 创建录制器
const { data: mediaRecorder, error: recorderError } = trySync({
try: () => new MediaRecorder(stream, { bitsPerSecond: bitrate }),
catch: (error) => RecorderServiceErr({
message: `初始化录制器失败。${extractErrorMessage(error)}`,
}),
});
if (recorderError) {
cleanupRecordingStream(stream); // 失败时清理
return Err(recorderError);
}
// 顺利继续快乐路径...
mediaRecorder.start(TIMESLICE_MS);
return Ok(deviceOutcome);
},
总结:包装指南
| 场景 | 方法 |
|---|---|
| 单一有风险操作 | 只包装该操作 |
| 顺序操作 | 分别包装每个,错误时立即返回 |
| 必须一起成功的原子操作 | 一起包装在一个块中 |
| 需要不同错误类型 | 用适当错误类型分开块 |
| 失败时需要清理 | 包装,检查错误,如果需要清理,然后返回 |
目标:每个trySync/tryAsync块应表示一个单一的“失败单元”,带有具体、描述性的错误消息。