DrillLab
第 12 / 17 节LESSON 12 / 17约 16 分钟~16 min

三处埋雷:怎么系统地找出来The three planted bugs: how to find them systematically

README 只说「有 integration issues」。这一节教你怎么把它们挖出来。The README only says there are integration issues. This lesson shows you how to find them.

2 个练习2 exercisesFederation · 第 3 部分Federation · Part 3
这一页有什么On this page8
学完这节你会After this lesson you can
  • 掌握一套「核对而非猜测」的排查流程Learn a debugging routine based on checking, not guessing
  • 独立找出并修复三处埋雷Find and fix the three planted bugs on your own
  • 把 Mutation.createOrder 改到测试通过Get Mutation.createOrder to pass its tests
  • 解释为什么这三个错误都「看起来很合理」Explain why all three bugs look reasonable at first
这在考试里考什么What the exam does with this

三处埋雷各挂一个测试。而且它们的错法很典型 —— 名字对不上、签名对不上、错误被吞掉。这三类问题在任何后端代码里都会遇到。Each planted bug fails one test. All three are common kinds of mistake: a name that does not match, a signature that does not match, and an error that gets swallowed. You meet these three in any backend code.

这节课要看的真实文件Real files this lesson looks at高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js三处埋雷都在这里All three planted bugs are in here

提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.

JavaScriptorderResolvers.js源项目From source
1import DataLoader from 'dataloader';
2import { GraphQLError } from 'graphql';
3
4// Custom error codes
5const ErrorCodes = {
6 ORDER_NOT_FOUND: 'ORDER_NOT_FOUND',
7 INVALID_INPUT: 'INVALID_INPUT',
8 INVENTORY_ERROR: 'INVENTORY_ERROR',
9 SERVICE_ERROR: 'SERVICE_ERROR'
10};
11
12// DataLoader for batching shipping info requests
13function createShippingInfoLoader(shippingDataSource) {
14 return new DataLoader(async orderIds => {
15 console.log(`[DataLoader] Batching ${orderIds.length} shipping info requests`);
16
17 const shippingInfos = await Promise.all(
18 orderIds.map(id => shippingDataSource.getShippingInfo(id))
19 );
20
21 return shippingInfos;
22 });
23}
24
25// DataLoader for batching order requests
26function createOrderLoader(orderDataSource) {
27 return new DataLoader(async orderIds => {
28 console.log(`[DataLoader] Batching ${orderIds.length} order requests`);
29
30 const orders = await Promise.all(
31 orderIds.map(id => orderDataSource.getOrderById(id))
32 );
33
34 return orders;
35 });
36}
37
38export const resolvers = {
39 User: {
40 // Reference Resolver - extend User entity from Accounts subgraph
41 __resolveReference(user, { dataSources, loaders }) {
42 return { id: user.id };
43 },
44
45 // Field resolver with caching
46 async orders(user, _, { dataSources, loaders, correlationId }) {
47 // TODO: Implement orders resolver with proper error handling and correlation ID tracing
48 return [];
49 }
50 },
51
52 Order: {
53 // Field resolver for shipping info with DataLoader
54 async shippingInfo(parent, _, { dataSources, loaders, correlationId }) {
55 // TODO: Implement shipping info resolver using DataLoader to prevent N+1 queries
56 return null;
57 }
58 },
59
60 Query: {
61 async order(_, { id }, { dataSources, loaders, correlationId }) {
62 // TODO: Implement order query using DataLoader with structured error handling
63 return null;
64 },
65
66 async orders(_, { userId }, { dataSources, correlationId }) {
67 // TODO: Implement orders query with error handling and correlation ID logging
68 return [];
69 }
70 },
71
72 Mutation: {
73 // Mutation provided for reference - candidates focus on Query resolvers
74 async createOrder(_, { userId, items }, { dataSources, correlationId }) {
75 try {
76 console.log(`[${correlationId}] Creating order for userId: ${userId}`);
77
78 if (!userId || !items || items.length === 0) {
79 throw new GraphQLError('Invalid order input', {
80 extensions: {
81 code: ErrorCodes.INVALID_INPUT,
82 correlationId
83 }
84 });
85 }
86
87 const order = await dataSources.orderAPI.createOrder({ userId, items });
88 console.log(`[${correlationId}] Order created: ${order.id}`);
89
90 return order;
91 } catch (error) {
92 console.error(`[${correlationId}] Error creating order:`, error.message);
93 throw new GraphQLError('Failed to create order', {
94 extensions: {
95 code: ErrorCodes.SERVICE_ERROR,
96 correlationId,
97 originalError: error.message
98 }
99 });
100 }
101 }
102 }
103};
104
105export { createShippingInfoLoader, createOrderLoader };
Source: graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js
graphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.js核对方法名与签名的依据(PROVIDED,别改)What you check method names and signatures against (PROVIDED, do not change)
JavaScriptorderDataSource.js源项目From source
1// Mock data sources simulating downstream microservices
2
3class OrderDataSource {
4 constructor() {
5 this.orders = [
6 {
7 id: 'order-456',
8 userId: '123',
9 status: 'SHIPPED',
10 totalAmount: 299.99,
11 items: [{ productId: 'prod-789', quantity: 2, price: 149.99 }],
12 createdAt: '2026-01-01T10:30:00Z'
13 },
14 {
15 id: 'order-457',
16 userId: '123',
17 status: 'DELIVERED',
18 totalAmount: 89.99,
19 items: [{ productId: 'prod-101', quantity: 1, price: 89.99 }],
20 createdAt: '2025-12-15T14:20:00Z'
21 },
22 {
23 id: 'order-458',
24 userId: '456',
25 status: 'PENDING',
26 totalAmount: 199.99,
27 items: [{ productId: 'prod-202', quantity: 1, price: 199.99 }],
28 createdAt: '2026-01-05T09:15:00Z'
29 }
30 ];
31 }
32
33 async getOrder(id) {
34 await new Promise(resolve => setTimeout(resolve, 10));
35 return this.orders.find(order => order.id === id);
36 }
37
38 async getOrdersByUserId(userId) {
39 await new Promise(resolve => setTimeout(resolve, 10));
40 return this.orders.filter(order => order.userId === userId);
41 }
42
43 async createOrder(userId, items) {
44 await new Promise(resolve => setTimeout(resolve, 10));
45
46 const totalAmount = items.reduce((sum, item) => {
47 return sum + item.price * item.quantity;
48 }, 0);
49
50 const newOrder = {
51 id: `order-${Date.now()}`,
52 userId,
53 status: 'PENDING',
54 totalAmount,
55 items,
56 createdAt: new Date().toISOString()
57 };
58
59 this.orders.push(newOrder);
60 return newOrder;
61 }
62}
63
64class InventoryDataSource {
65 async getInventoryStatus(productIds) {
66 await new Promise(resolve => setTimeout(resolve, 10));
67 return productIds.map(id => ({ productId: id, inStock: true, quantity: 100 }));
68 }
69
70 async getProductPrice(productId) {
71 await new Promise(resolve => setTimeout(resolve, 5));
72 const prices = {
73 'prod-789': 149.99,
74 'prod-101': 89.99,
75 'prod-202': 199.99
76 };
77 return prices[productId] || 99.99;
78 }
79}
80
81class ShippingDataSource {
82 async getShippingInfo(orderId) {
83 await new Promise(resolve => setTimeout(resolve, 10));
84
85 const shippingData = {
86 'order-456': {
87 status: 'IN_TRANSIT',
88 estimatedDelivery: '2026-01-10',
89 trackingNumber: 'TRACK123456'
90 },
91 'order-457': {
92 status: 'DELIVERED',
93 estimatedDelivery: '2025-12-20',
94 trackingNumber: 'TRACK123457'
95 }
96 };
97
98 return shippingData[orderId] || null;
99 }
100}
101
102export { OrderDataSource, InventoryDataSource, ShippingDataSource };
Source: graphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.js
§01

排查方法:三张对照表The method: three reference tables

不要靠读代码「感觉哪里怪」。逐项核对。Do not read the code looking for something that feels off. Check item by item.

README 说有 integration issues,但不说在哪。 系统的做法是核对三件事

  1. 每一处 context.xxx 的键名, 对照 index.js 里 context 函数的 return。
  2. 每一处数据源方法调用的名字和参数, 对照 dataSources/orderDataSource.js 里的类定义。
  3. 每一处 throwcatch 的配对, 看有没有「自己抛的错被自己吞掉」。

这三项核对能找出全部三处埋雷。而且这套方法在任何项目里都管用 —— 「跨模块的名字和签名」是所有集成 bug 的高发区。

更快的办法:先跑测试,看报错指向哪一行。三处埋雷各自挂一个测试,报错信息都很直接。 但你得能看懂报错说的是什么。

The README says there are integration issues but not where they are. The systematic approach is to cross-check three things:

  1. Every key name in a context.xxx access, against what the context function in index.js returns.
  2. Every data source method call, name and arguments, against the class definitions in dataSources/orderDataSource.js.
  3. Every pairing of throw and catch, looking for an error you threw being swallowed by your own handler.

Those three checks find all three planted bugs. And the method works in any project — names and signatures that cross module boundaries are where integration bugs live.

The faster route: run the tests first and see which line the errors point at. Each planted bug takes down one test, and the messages are direct. But you have to be able to read what they are saying.

§02

埋雷 1 · getOrderById 不存在Planted bug 1 · getOrderById does not exist

报错:TypeError: orderDataSource.getOrderById is not a function

位置:createOrderLoader 的 batch 函数。

核对:OrderDataSource 上只有getOrdergetOrdersByUserIdcreateOrder

修法:把调用改成 getOrder(id)不是给数据源加方法 —— 那个文件是 PROVIDED。

为什么容易犯:getOrderById 是个再自然不过的名字。 很多项目就叫这个。靠直觉写就中招。

The error: TypeError: orderDataSource.getOrderById is not a function

Where: the batch function of createOrderLoader.

Cross-check: OrderDataSource only has getOrder, getOrdersByUserId and createOrder.

The fix: change the call to getOrder(id). Not adding a method to the data source — that file is PROVIDED.

Why it is easy to fall for: getOrderById is about as natural a name as there is. Plenty of projects call it exactly that. Write on instinct and you are caught.

JavaScript埋雷 1 的修复The fix for planted bug 1示意Illustrative
1// 前
2orderIds.map(id => orderDataSource.getOrderById(id))
3
4// 后
5orderIds.map(id => orderDataSource.getOrder(id))
1// before
2orderIds.map(id => orderDataSource.getOrderById(id))
3
4// after
5orderIds.map(id => orderDataSource.getOrder(id))
§03

埋雷 2 · orderAPI 不存在,而且签名也错了Planted bug 2 · orderAPI does not exist, and the signature is wrong too

这一处其实是三个错叠在一起。This one is really three mistakes stacked on top of each other.

报错:Cannot read properties of undefined (reading 'createOrder')

原始代码:await dataSources.orderAPI.createOrder({ userId, items })

三处问题:

  1. 键名错。context 里是orderDataSource,没有 orderAPI。 所以 dataSources.orderAPIundefined, 在它上面取 .createOrder 就抛了。
  2. 签名错。真实签名是 createOrder(userId, items) ——两个位置参数,不是一个对象。 传对象进去,userId 会是那个对象,items 会是 undefined。
  3. 缺一步。OrderItemInput 里没有 price, 而数据源要用 item.price 算总价。resolver 必须先去查价格。

第 3 点是最隐蔽的 —— 前两点报错很直接, 第 3 点即使前两点修好了,也只会表现为totalAmount 是 NaN、items[0].price 是 undefined。 测试用expect(order.items[0].price).toBeDefined()expect(order.totalAmount).toBeGreaterThan(0)两条断言抓它。

The error: Cannot read properties of undefined (reading 'createOrder')

The original code: await dataSources.orderAPI.createOrder({ userId, items })

Three problems in one line:

  1. Wrong key name. context has orderDataSource, there is no orderAPI. So dataSources.orderAPI is undefined, and reading .createOrder off it throws.
  2. Wrong signature. The real signature is createOrder(userId, items) two positional arguments, not one object. Pass an object and userId becomes that object while items becomes undefined.
  3. A missing step. OrderItemInput has no price, and the data source needs item.price to compute the total. The resolver has to look the price up first.

Number 3 is the sneaky one — the first two throw loudly, while the third, even after the other two are fixed, only shows up as totalAmount being NaN and items[0].price being undefined. The tests catch it with two assertions, expect(order.items[0].price).toBeDefined() and expect(order.totalAmount).toBeGreaterThan(0).

JavaScript核对依据What you check against源项目From source
1// 数据源的真实签名与内部实现
2async createOrder(userId, items) { // ← 两个位置参数
3 const totalAmount = items.reduce((sum, item) => {
4 return sum + item.price * item.quantity; // ← 需要 item.price
5 }, 0);
6 ...
7}
1// The real signature of the data source, and what it does inside
2async createOrder(userId, items) { // ← two positional arguments
3 const totalAmount = items.reduce((sum, item) => {
4 return sum + item.price * item.quantity; // ← it needs item.price
5 }, 0);
6 ...
7}
Source: graphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.js
JavaScript埋雷 2 的修复The fix for planted bug 2已跑通Verified
1// 修好之后:先补 price,再用正确的键名和签名
2const pricedItems = await Promise.all(
3 items.map(async item => ({
4 productId: item.productId,
5 quantity: item.quantity,
6 price: await dataSources.inventoryDataSource.getProductPrice(item.productId)
7 }))
8);
9
10const order = await dataSources.orderDataSource.createOrder(userId, pricedItems);
1// After the fix: look up price first, then use the right key name and signature
2const pricedItems = await Promise.all(
3 items.map(async item => ({
4 productId: item.productId,
5 quantity: item.quantity,
6 price: await dataSources.inventoryDataSource.getProductPrice(item.productId)
7 }))
8);
9
10const order = await dataSources.orderDataSource.createOrder(userId, pricedItems);
map 的回调是 async,所以必须外套 Promise.all —— 这是 Foundations 那门课讲过的固定套路。The map callback is async, so it has to be wrapped in Promise.all. This is the fixed pattern the Foundations course covers.
§04

埋雷 3 · catch 把 INVALID_INPUT 吞成了 SERVICE_ERRORPlanted bug 3 · catch turns INVALID_INPUT into SERVICE_ERROR

这一处不报错,只是错误码不对。Nothing crashes here. Only the error code is wrong.

测试报错:

病因:try 块里先抛了GraphQLError(code: INVALID_INPUT), 紧接着自己的 catch 把它接住, 重新包成 code: SERVICE_ERROR

修法:catch 第一行加if (error instanceof GraphQLError) throw error;

为什么这是最重要的一处:前两处是「打错字」级别的错误,报错很直接。 这一处是设计缺陷 —— 代码能跑、不抛异常、只是给客户端的信号是错的。这类 bug 在生产环境里能藏几个月: 客户端一直在重试「输入不合法」的请求, 监控看到的是「服务错误率高」, 实际是校验失败被误报成了系统故障。

What the test reports:

The cause: the try block throws GraphQLError(code: INVALID_INPUT), and its own catch immediately grabs it and rewraps it as code: SERVICE_ERROR.

The fix: make the first line of the catch if (error instanceof GraphQLError) throw error;

Why this is the most important one of the three: the first two are typo-grade mistakes with direct error messages. This one is a design flaw — the code runs, throws nothing, and merely sends the client the wrong signal. This class of bug can hide in production for months: clients keep retrying requests that were invalid input, monitoring shows a high service error rate, and the real story is validation failures misreported as system faults.

Terminal本机实测的报错The failure as it really appears locally已跑通Verified
1Order ResolversError handlingshould return structured error for validation failures
2
3 expect(received).toBe(expected) // Object.is equality
4
5 Expected: "INVALID_INPUT"
6 Received: "SERVICE_ERROR"
7
8 137 | } catch (error) {
9 138 | expect(error.extensions).toBeDefined();
10 > 139 | expect(error.extensions.code).toBe('INVALID_INPUT');
JavaScript埋雷 3 的修复(一行)The fix for planted bug 3 (one line)示意Illustrative
1// 前:自己抛的错被自己吞了
2} catch (error) {
3 console.error(`[${correlationId}] Error creating order:`, error.message);
4 throw new GraphQLError('Failed to create order', {
5 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId, originalError: error.message }
6 });
7}
8
9// 后:先放行已经结构化的错误
10} catch (error) {
11 if (error instanceof GraphQLError) throw error;
12
13 console.error(`[${correlationId}] Error creating order:`, error.message);
14 throw new GraphQLError('Failed to create order', {
15 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId, originalError: error.message }
16 });
17}
1// before: your own error gets swallowed by your own catch
2} catch (error) {
3 console.error(`[${correlationId}] Error creating order:`, error.message);
4 throw new GraphQLError('Failed to create order', {
5 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId, originalError: error.message }
6 });
7}
8
9// after: let an already structured error through first
10} catch (error) {
11 if (error instanceof GraphQLError) throw error;
12
13 console.error(`[${correlationId}] Error creating order:`, error.message);
14 throw new GraphQLError('Failed to create order', {
15 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId, originalError: error.message }
16 });
17}
§05

Mutation.createOrder 的完整修复版The fully fixed Mutation.createOrder

三处埋雷有两处在这个函数里。Two of the three planted bugs are inside this one function.

Mutation.createOrder 的注释写着 「provided for reference - candidates focus on Query resolvers」,但它是坏的。 「给你参考」不等于「它是对的」—— 这也是这个项目的一个小陷阱。

审计实测:这样改完之后 10 个测试全部通过。

The comment on Mutation.createOrder reads “provided for reference - candidates focus on Query resolvers”, and yet it is broken. “Here for reference” does not mean “this is correct” — another small trap in this project.

Measured in the audit: with these changes all ten tests pass.

JavaScriptMutation.createOrder(修复后,实测 10/10 通过)Mutation.createOrder (after the fixes, measured 10/10 passing)已跑通Verified
1async createOrder(_, { userId, items }, { dataSources, correlationId }) {
2 try {
3 console.log(`[${correlationId}] Creating order for userId: ${userId}`);
4
5 if (!userId || !items || items.length === 0) {
6 throw new GraphQLError('Invalid order input', {
7 extensions: {
8 code: ErrorCodes.INVALID_INPUT,
9 correlationId
10 }
11 });
12 }
13
14 // OrderItemInput 只带 productId + quantity。
15 // OrderDataSource.createOrder 内部要算 item.price * item.quantity,
16 // 所以必须先去库存服务把 price 查出来。
17 const pricedItems = await Promise.all(
18 items.map(async item => ({
19 productId: item.productId,
20 quantity: item.quantity,
21 price: await dataSources.inventoryDataSource.getProductPrice(item.productId)
22 }))
23 );
24
25 // FIX: context 里叫 orderDataSource(不是 orderAPI),
26 // 签名是 createOrder(userId, items) 两个位置参数。
27 const order = await dataSources.orderDataSource.createOrder(userId, pricedItems);
28 console.log(`[${correlationId}] Order created: ${order.id}`);
29
30 return order;
31 } catch (error) {
32 // FIX: 已经是结构化 GraphQLError 的不要重新包装,
33 // 否则 INVALID_INPUT 会以 SERVICE_ERROR 的形式到客户端。
34 if (error instanceof GraphQLError) throw error;
35
36 console.error(`[${correlationId}] Error creating order:`, error.message);
37 throw new GraphQLError('Failed to create order', {
38 extensions: {
39 code: ErrorCodes.SERVICE_ERROR,
40 correlationId,
41 originalError: error.message
42 }
43 });
44 }
45}
1async createOrder(_, { userId, items }, { dataSources, correlationId }) {
2 try {
3 console.log(`[${correlationId}] Creating order for userId: ${userId}`);
4
5 if (!userId || !items || items.length === 0) {
6 throw new GraphQLError('Invalid order input', {
7 extensions: {
8 code: ErrorCodes.INVALID_INPUT,
9 correlationId
10 }
11 });
12 }
13
14 // OrderItemInput only carries productId + quantity.
15 // OrderDataSource.createOrder computes item.price * item.quantity inside,
16 // so the price has to be fetched from the inventory service first.
17 const pricedItems = await Promise.all(
18 items.map(async item => ({
19 productId: item.productId,
20 quantity: item.quantity,
21 price: await dataSources.inventoryDataSource.getProductPrice(item.productId)
22 }))
23 );
24
25 // FIX: in context it is called orderDataSource, not orderAPI,
26 // and the signature is createOrder(userId, items) — two positional arguments.
27 const order = await dataSources.orderDataSource.createOrder(userId, pricedItems);
28 console.log(`[${correlationId}] Order created: ${order.id}`);
29
30 return order;
31 } catch (error) {
32 // FIX: do not rewrap an error that is already a structured GraphQLError,
33 // or INVALID_INPUT reaches the client as SERVICE_ERROR.
34 if (error instanceof GraphQLError) throw error;
35
36 console.error(`[${correlationId}] Error creating order:`, error.message);
37 throw new GraphQLError('Failed to create order', {
38 extensions: {
39 code: ErrorCodes.SERVICE_ERROR,
40 correlationId,
41 originalError: error.message
42 }
43 });
44 }
45}
Terminal审计时的真实输出(参考解法)The real output from the audit (with the reference answer)已跑通Verified
1$ npm test
2
3 Order Resolvers
4 User.orders resolver
5should return orders for a user (22 ms)
6should return empty array for user with no orders (12 ms)
7 Order.shippingInfo resolver
8should return shipping info for an order (13 ms)
9should return null for order without shipping info (12 ms)
10 Query.orders resolver
11should return orders for a specific user (12 ms)
12should return empty array for user with no orders (12 ms)
13 Mutation.createOrder resolver
14should create a new order successfully (20 ms)
15 DataLoader functionality
16should batch multiple order requests (12 ms)
17should batch multiple shipping info requests (13 ms)
18 Error handling
19should return structured error for validation failures (1 ms)
20
21Test Suites: 1 passed, 1 total
22Tests: 10 passed, 10 total
§06

为什么这三个错都「看起来很合理」Why all three bugs look reasonable

出题人选这三处不是随机的。它们的共同点是「读代码时不会觉得奇怪」

  • getOrderById —— 比 getOrder更符合常见命名习惯。
  • orderAPI —— Apollo 老版本的 DataSource 就常叫 xxxAPI
  • createOrder({ userId, items }) —— 「参数打包成对象」是现代 JS 的流行风格。
  • catch 里统一包装错误 —— 这是好实践, 只是漏了一个例外情况。

所以「读一遍觉得没问题」是不够的。必须核对。这也是为什么本门课反复强调 「写代码前先抄一张方法名对照表」—— 那五分钟能省下半小时的困惑。

The examiner did not pick these three spots at random. What they share is that nothing looks odd while you are reading:

  • getOrderById — a closer fit to common naming habits than getOrder.
  • orderAPI — older Apollo DataSources were often named xxxAPI.
  • createOrder({ userId, items }) — packing arguments into an object is a popular modern JS style.
  • wrapping every error in the catch — that is good practice, it just misses one exception.

So “I read it and it seemed fine” is not enough. You have to cross-check. Which is why this course keeps repeating “copy out a table of method names before you write code” — those five minutes save half an hour of confusion.

练习Practice

动手做Get your hands on it

填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.

L3Debug LabDebug LabDebug Lab · Cannot read properties of undefinedDebug Lab · Cannot read properties of undefined

Mutation.createOrder 的测试挂了。 报错说在读一个 undefined 的属性。自己分诊。

The Mutation.createOrder test fails. The error says something read a property of undefined. Diagnose it yourself.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
● Order Resolvers › Mutation.createOrder resolver › should create a new order successfully GraphQLError: Failed to create order 91 | } catch (error) { 92 | console.error(`[${correlationId}] Error creating order:`, error.message); > 93 | throw new GraphQLError('Failed to create order', { # 往上翻,console.error 打出的原始错误是: console.error [test-correlation-id] Error creating order: Cannot read properties of undefined (reading 'createOrder')● Order Resolvers › Mutation.createOrder resolver › should create a new order successfully GraphQLError: Failed to create order 91 | } catch (error) { 92 | console.error(`[${correlationId}] Error creating order:`, error.message); > 93 | throw new GraphQLError('Failed to create order', { # Scroll up: the raw error that console.error printed is console.error [test-correlation-id] Error creating order: Cannot read properties of undefined (reading 'createOrder')
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1const order = await dataSources.orderAPI.createOrder({ userId, items });
2
3// 参考:index.js 里 context 的 return
4// return {
5// dataSources: { orderDataSource, inventoryDataSource, shippingDataSource },
6// loaders: { shippingInfoLoader, orderLoader },
7// correlationId
8// };
1const order = await dataSources.orderAPI.createOrder({ userId, items });
2
3// For reference: what the context function in index.js returns
4// return {
5// dataSources: { orderDataSource, inventoryDataSource, shippingDataSource },
6// loaders: { shippingInfoLoader, orderLoader },
7// correlationId
8// };
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
L3Debug LabDebug LabDebug Lab · 错误码不对(不报错的那种 bug)Debug Lab · The wrong error code (the kind of bug that throws nothing)

代码跑得通,没有异常。但测试说错误码不对。 这是三处埋雷里最值得理解的一处。

The code runs and raises no exception, but the test says the error code is wrong. Of the three planted bugs, this is the one most worth understanding.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
● Order Resolvers › Error handling › should return structured error for validation failures expect(received).toBe(expected) // Object.is equality Expected: "INVALID_INPUT" Received: "SERVICE_ERROR" # 测试代码: # const input = { userId: '789', items: [] }; ← 空 items,应该被校验拦下 # try { # await resolvers.Mutation.createOrder({}, input, context); # throw new Error('Should have thrown an error'); # } catch (error) { # expect(error.extensions.code).toBe('INVALID_INPUT'); # }● Order Resolvers › Error handling › should return structured error for validation failures expect(received).toBe(expected) // Object.is equality Expected: "INVALID_INPUT" Received: "SERVICE_ERROR" # The test code: # const input = { userId: '789', items: [] }; ← empty items, validation should stop it # try { # await resolvers.Mutation.createOrder({}, input, context); # throw new Error('Should have thrown an error'); # } catch (error) { # expect(error.extensions.code).toBe('INVALID_INPUT'); # }
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1try {
2 if (!userId || !items || items.length === 0) {
3 throw new GraphQLError('Invalid order input', {
4 extensions: { code: ErrorCodes.INVALID_INPUT, correlationId }
5 });
6 }
7 const order = await dataSources.orderDataSource.createOrder(userId, pricedItems);
8 return order;
9} catch (error) {
10 console.error(`[${correlationId}] Error creating order:`, error.message);
11 throw new GraphQLError('Failed to create order', {
12 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId }
13 });
14}
1try {
2 if (!userId || !items || items.length === 0) {
3 throw new GraphQLError('Invalid order input', {
4 extensions: { code: ErrorCodes.INVALID_INPUT, correlationId }
5 });
6 }
7 const order = await dataSources.orderDataSource.createOrder(userId, pricedItems);
8 return order;
9} catch (error) {
10 console.error(`[${correlationId}] Error creating order:`, error.message);
11 throw new GraphQLError('Failed to create order', {
12 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId }
13 });
14}
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

README 说有「integration issues」The README mentions integration issues
核对三张表:context 键名、方法名与签名、throw/catch 配对Check three tables: context key names, method names and signatures, and how throw pairs with catch
xxx is not a functionxxx is not a function
去被调对象的定义里核对方法名Open the definition of the object you called and check the method name
Cannot read properties of undefinedCannot read properties of undefined
上一级路径写错了,逐段核对One step earlier in the path is wrong; check each part
自己包装的错误掩盖了真实原因Your own wrapper hides the real cause
往上翻原始 message;包装时保留 originalErrorLook further up for the original message; keep originalError when wrapping
catch 里统一包装错误A catch block wraps every error the same way
第一行先 if (error instanceof XxxError) throw errorFirst line: if (error instanceof XxxError) throw error
这节的要点What to take away
  1. 三处埋雷:getOrderById 不存在、orderAPI 不存在且签名错且缺 price、catch 吞掉 INVALID_INPUT。The three planted bugs: getOrderById does not exist; orderAPI does not exist, its signature is wrong and price is missing; catch swallows INVALID_INPUT.
  2. 排查靠核对三张表,不靠「读一遍感觉哪里怪」—— 这三个错都看起来很合理。Find them by checking the three tables, not by reading once and looking for something odd. All three look reasonable.
  3. 只改 EDIT THIS 的文件;给数据源加方法是错的修法。Only change files marked EDIT THIS. Adding a method to the data source is the wrong fix.
  4. 自己包装错误时保留 originalError,否则真实原因彻底丢失。Keep originalError when you wrap an error, otherwise the real cause is lost for good.
  5. catch 里统一包装错误时,第一行必须先放行已结构化的错误。When a catch block wraps every error, its first line must let already structured errors through.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises2 个,就在这一页上面 —— 别攒着最后一起做2 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lesson先看懂给你的东西:Spring 的几个注解和一条请求链路Understand what you are given: a few Spring annotations and the path one request takes
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: TODO 3 & 4 · Query.order 与 Query.ordersTODO 3 & 4 · Query.order and Query.orders