异常捕获的设计难题
在微服务调用链路中,异常可以在多个层面发生。关键问题在于:在哪里捕获 gRPC 调用异常最合理?
- 在拦截器中捕获?不行——拦截器无法知道当前调用的是哪个微服务
- 在每个方法中加 try/catch?太繁琐——一个 Service 可能有几十个方法
- 最优方案:在
promisifyServiceMethod公共方法中统一捕获
promisifyServiceMethod 改造
函数重载设计
为了支持多种调用方式,使用 TypeScript 函数重载:
// src/proto-pkg/src/grpc-utils.ts
// 重载 1:传入 error 回调
export function promisifyServiceMethods(
service: any,
errorCallback: (error: any) => void,
): void;
// 重载 2:传入白名单(不需要错误回调)
export function promisifyServiceMethods(
service: any,
whiteList?: string,
): void;
// 重载 3:同时传入错误回调和白名单
export function promisifyServiceMethods(
service: any,
errorCallback: (error: any) => void,
whiteList: string,
): void;
typescript
实现逻辑
export function promisifyServiceMethods(
service: any,
errorCallbackOrWhiteList?: ((error: any) => void) | string,
whiteList?: string,
): void {
let wList = '';
let errorCallbackFunction: (error: any) => void = () => {};
// 参数解析
if (Array.isArray(errorCallbackOrWhiteList)) {
wList = errorCallbackOrWhiteList as string;
} else if (typeof errorCallbackOrWhiteList === 'function') {
errorCallbackFunction = errorCallbackOrWhiteList;
}
// 遍历 service 上的所有方法
for (const methodName in service) {
if (typeof service[methodName] !== 'function') continue;
if (wList && !wList.includes(methodName)) continue;
const originalMethod = service[methodName];
service[methodName] = async (...args: any[]) => {
try {
// 关键:使用 await 等待 Promise 完成
return await firstValueFrom(originalMethod.apply(service, args));
} catch (error) {
// 通过回调将错误暴露给调用方
errorCallbackFunction(error);
// 同时将错误继续抛出
throw error;
}
};
}
}
typescript
关键修复:firstValueFrom + await
原先的代码中,firstValueFrom() 返回的是 Promise,但没有加 await,导致 try/catch 无法捕获异步错误:
// ❌ 错误:没有 await,catch 永远不会触发
try {
return firstValueFrom(originalMethod.apply(service, args));
} catch (error) {
// 永远不会执行到这里
}
// ✅ 正确:加上 await,Promise 被 reject 时 catch 能捕获
try {
return await firstValueFrom(originalMethod.apply(service, args));
} catch (error) {
errorCallbackFunction(error);
throw error;
}
typescript
在 AuthService 中使用
// src/modules/auth/auth.service.ts
@Injectable()
export class AuthService implements OnModuleInit {
private userService: any;
constructor(
@Inject('nestjs-user-service')
private readonly grpcClient: () => Promise<GrpcClientInterface>,
) {}
async onModuleInit() {
const clientInterface = await this.grpcClient();
if (!clientInterface?.client) return;
clientInterface.client.subscribe((client: ClientGrpc | null) => {
if (!client) return;
this.userService = client.getService('UserService');
// 使用带错误回调的 promisifyServiceMethods
promisifyServiceMethods(
this.userService,
(error) => {
console.error('gRPC call failed:', error);
// 这里可以触发 ConsulService 更新客户端实例
// TODO: 切换到健康的实例
},
);
});
}
}
typescript
构建 proto-pkg
由于 promisifyServiceMethod 在共享包 proto-pkg 中,修改后需要重新构建:
cd libs/proto-pkg
pnpm build
# 验证构建结果
# 检查 dist/index.d.ts 中是否包含新的重载声明
grep "promisifyServiceMethods" dist/index.d.ts
bash
如果 TypeScript 类型声明不完整,需要在源文件中显式写出所有重载签名,然后重新构建。
错误处理的完整链路
1. gRPC 服务调用失败
↓
2. firstValueFrom() 返回的 Promise 被 reject
↓
3. await 将 reject 转为 throw
↓
4. promisifyServiceMethod 的 catch 捕获
↓
5. errorCallbackFunction(error) → 通知 AuthService
↓
6. throw error → 错误继续向上抛出
↓
7. GrpcRetryInterceptor 捕获 → 触发重试机制
text
下一步
错误回调提供了在服务调用失败时更新 gRPC 客户端实例的入口。下一步需要实现 updateService 方法,在捕获到错误后自动切换到健康的 Consul 实例。
↑