TypeScript客户端提取ApolloError消息的技术问询
我正在学习TypeScript和Apollo Server,现在遇到一个问题:在处理addUser突变时,要求用户名和邮箱唯一,验证通过时正常,但验证失败(比如重复键)时,无法从ApolloError中提取错误消息返回给客户端。MongoDB返回的错误信息类似:
duplicate key error collection: be_for_reel_db.users index: userName_1 dup key: { userName: "foo" }
但前端无法提取这段文本提示用户。技术栈:React 18、Apollo Server Express 3.6.2、GraphQL 16.3.0。
客户端代码
客户端用try-catch处理突变:
try { const { data } = await addUser({ variables: { userName: signupForm.signupUsername, email: signupForm.signupEmail, password: signupForm.signupPassword, }, }); } catch (err) { setErrorMessage(true); console.log(err); }
这里console.log能看到ApolloError,但TypeScript报错"err" is of type unknown,无法直接访问err.message。
已尝试的方法
- 尝试从Apollo客户端导入ApolloError作为类型,编译报错。
- 服务器端用
formatError自定义错误:
自定义消息能返回,但前端还是无法访问。const server = new ApolloServer({ typeDefs, resolvers, context: authMiddleware, formatError: (formattedError, error) => { return { message: "This is my custom error" }; }, }); - 解析器中添加逻辑:
但突变失败时Apollo直接抛出错误,剩余代码不会执行,Mutation: { addUser: async (parent, args) => { const { userName, email, password } = args; const today = Date(); const newUser = { userName, email, password, created: today, polls: [], votes: [], comments: [], }; const user = await User.create(newUser); if (!user) return { message: "Operation failed" }; const token = signToken(user); return { token, user }; }, // 其他突变 }if (!user)永远走不到。
后续尝试
按建议移除客户端try-catch,错误直接显示在浏览器控制台,但还是无法提取。又在服务器端解析错误:
try { const user = await User.create(newUser); return user; } catch (err) { const pieces = err.message.split(": "); let message = pieces.pop(); message = `${pieces.pop()}: ${message}`; return message; }
解析后能生成类似{ username: foo }的消息,但返回客户端后还是被包裹在ApolloError里,前端依旧无法访问。
解决方案
1. 服务器端正确捕获并格式化MongoDB重复键错误
在解析器中,需要捕获MongoDB的重复键错误(错误码11000),然后抛出GraphQL错误,而不是直接返回字符串。这样Apollo Server会正确处理错误,并传递给客户端:
import { ApolloError } from 'apollo-server-express'; Mutation: { addUser: async (parent, args) => { const { userName, email, password } = args; const today = new Date().toISOString(); // 建议用标准格式的时间字符串 const newUser = { userName, email, password, created: today, polls: [], votes: [], comments: [], }; try { const user = await User.create(newUser); const token = signToken(user); return { token, user }; } catch (err: any) { // 捕获MongoDB重复键错误 if (err.code === 11000) { // 提取重复的字段(userName或email) const duplicateField = Object.keys(err.keyPattern)[0]; throw new ApolloError( `该${duplicateField === 'userName' ? '用户名' : '邮箱'}已被占用`, 'DUPLICATE_FIELD' ); } // 其他错误统一处理 throw new ApolloError('创建用户失败,请稍后重试'); } }, }
2. 客户端正确处理ApolloError并提取消息
在客户端,需要先判断错误类型,TypeScript中可以用类型守卫来安全访问错误属性:
import { ApolloError } from '@apollo/client'; try { const { data } = await addUser({ variables: { userName: signupForm.signupUsername, email: signupForm.signupEmail, password: signupForm.signupPassword, }, }); // 处理用户创建成功后的逻辑,比如跳转、存储token等 } catch (err) { if (err instanceof ApolloError) { // 提取错误消息并展示给用户 setErrorMessage(err.message); console.log('错误详情:', err); } else { setErrorMessage('未知错误,请稍后重试'); } }
3. 优化服务器端formatError(可选)
如果需要统一格式化所有错误,确保客户端能拿到标准结构的错误信息,可以配置formatError:
const server = new ApolloServer({ typeDefs, resolvers, context: authMiddleware, formatError: (formattedError) => { return { message: formattedError.message, code: formattedError.extensions?.code || 'UNKNOWN_ERROR', ...(formattedError.extensions?.exception && { details: formattedError.extensions.exception }) }; }, });
这样客户端不仅能拿到错误消息,还能根据code字段做针对性处理(比如针对DUPLICATE_FIELD错误做特殊提示样式)。
内容的提问来源于stack exchange,提问作者lkalliance

