Code do AI viết hiếm khi sai vì cú pháp — nó sai vì AI lặp lại đúng những thói quen xấu phổ biến nhất trong hàng triệu dòng code nó từng học, và nếu bạn thuộc lòng bảy "chữ ký lỗi" đó, bạn sẽ bắt được bug trước khi nó chạm production.
Vì Sao Code Do AI Sinh Ra Có Các Failure Mode Có Thể Đoán Trước
Khi một kỹ sư senior viết code sai, lỗi thường ngẫu nhiên — hôm nay quên xử lý null, mai lại quên đóng connection, tùy vào việc họ đang mệt hay đang vội. Nhưng lỗi trong code do LLM sinh ra lại khác: nó có tính hệ thống. Một LLM như GPT hay Claude không "suy nghĩ" về logic nghiệp vụ của bạn — nó dự đoán token tiếp theo dựa trên phân phối xác suất được học từ hàng tỷ dòng code public. Hệ quả trực tiếp: nếu một pattern nào đó xuất hiện phổ biến trong dữ liệu huấn luyện (kể cả khi pattern đó là anti-pattern), model có xu hướng tái tạo lại nó với tần suất cao, bất kể đúng sai.
Có ba nguyên nhân gốc rễ khiến điều này xảy ra:
Thứ nhất, thiên lệch dữ liệu huấn luyện (training data bias). Rất nhiều code trên GitHub là code demo, tutorial, hoặc proof-of-concept — nơi người viết cố tình bỏ qua error handling để giữ ví dụ ngắn gọn, dễ đọc. Model học luôn cả thói quen "code cho đẹp" đó và áp dụng vào code production, nơi lẽ ra phải xử lý chặt chẽ hơn nhiều.
Thứ hai, thiếu ngữ cảnh runtime. AI không chạy code trong đầu nó theo nghĩa thực thi từng bước với dữ liệu thật — nó sinh ra thứ "trông hợp lý" về mặt cú pháp và luồng logic bề mặt. Những lỗi chỉ lộ ra khi có dữ liệu biên (edge case), tải cao, hoặc race condition thường bị bỏ sót vì AI không mô phỏng được các điều kiện đó.
Thứ ba, tối ưu hóa cho "trông đúng" hơn là "chạy đúng". Model được huấn luyện để sinh ra output mà con người đánh giá là hợp lý, mạch lạc, đúng convention — không phải để tối đa hóa độ chính xác runtime. Một đoạn try/catch bọc toàn bộ hàm trông "an toàn" và "chuyên nghiệp" với người đọc thoáng qua, dù nó có thể nuốt mất lỗi nghiêm trọng.
Hiểu được cơ chế này quan trọng vì nó thay đổi cách bạn review: thay vì đọc từng dòng tìm lỗi ngẫu nhiên, bạn có thể quét theo checklist các pattern đã biết trước — giống như bác sĩ chẩn đoán theo triệu chứng điển hình thay vì đoán mò. Đây chính là kỹ năng cốt lõi của root cause analysis trong thời đại agentic engineering: biết trước "nghi phạm" thường gặp để tra cứu nhanh, thay vì dò từ đầu.
Mẹo: Trước khi review bất kỳ đoạn code nào do AI sinh ra, tự hỏi "đây có phải chỗ dễ bị AI code kiểu tutorial-demo không?" — error handling, boundary condition, và resource cleanup là ba khu vực rủi ro cao nhất.
Bảy Failure Pattern Phổ Biến Nhất Của Code Do AI Sinh Ra
1. Thiếu Error Propagation
BEFORE — code AI thường sinh ra:
async function getUserProfile(userId: string) {
const user = await db.users.findById(userId);
const preferences = await db.preferences.findByUserId(userId);
return { ...user, preferences };
}
// Nơi gọi
app.get('/profile/:id', async (req, res) => {
const profile = await getUserProfile(req.params.id);
res.json(profile);
});
Nhìn qua thì ổn — nhưng nếu db.preferences.findByUserId throw lỗi (mất kết nối DB, timeout), lỗi sẽ trôi lên tới Express mà không có context gì về việc nó xảy ra ở bước nào, và endpoint trả về lỗi 500 chung chung không giúp ích gì cho debugging sau này.
Vì sao AI hay sinh pattern này: phần lớn code mẫu về async/await trong dữ liệu huấn luyện tập trung vào "happy path" để minh họa cú pháp await cho rõ ràng, súc tích. AI học rằng "gọi async function thì cứ await thẳng, không cần bọc thêm gì" vì đó là cách viết ví dụ đẹp nhất — nhưng lại thiếu tính production-ready.
AFTER — có error propagation rõ ràng:
async function getUserProfile(userId: string) {
try {
const user = await db.users.findById(userId);
if (!user) {
throw new NotFoundError(`User ${userId} not found`);
}
const preferences = await db.preferences.findByUserId(userId);
return { ...user, preferences };
} catch (err) {
throw new ProfileFetchError(`Failed to load profile for ${userId}`, { cause: err });
}
}
app.get('/profile/:id', async (req, res, next) => {
try {
const profile = await getUserProfile(req.params.id);
res.json(profile);
} catch (err) {
next(err); // để middleware xử lý lỗi tập trung quyết định status code, log
}
});
Prompt để tránh: yêu cầu AI cụ thể "wrap each async call with contextual error handling, and ensure errors propagate to a centralized error handler — do not swallow or return generic responses on failure."
2. try/catch Quá Rộng
BEFORE:
def process_payment(order_id, amount, card_token):
try:
validate_order(order_id)
charge_result = payment_gateway.charge(card_token, amount)
update_order_status(order_id, "paid")
send_receipt_email(order_id)
return {"success": True, "charge_id": charge_result.id}
except Exception as e:
logger.error(f"Payment failed: {e}")
return {"success": False}
Vấn đề: nếu send_receipt_email fail (SMTP server down) sau khi tiền đã bị charge thành công, hàm này vẫn trả về {"success": False} — client hiểu nhầm là giao dịch thất bại, có thể khiến người dùng bấm thanh toán lại và bị charge hai lần! Một try/catch (hay try/except) bao trùm toàn bộ chuỗi hành động khác nhau về bản chất (validate, charge tiền thật, cập nhật DB, gửi email) là cực kỳ nguy hiểm.
Vì sao AI hay sinh pattern này: AI tối ưu cho "code trông gọn, không văng exception ra ngoài" — một khối try/catch to bao mọi thứ trông "an toàn" hơn nhiều khối nhỏ rải rác, đặc biệt khi prompt chỉ nói chung chung kiểu "handle errors properly" mà không chỉ rõ ranh giới rủi ro khác nhau giữa các bước.
AFTER — tách theo mức độ nghiêm trọng và khả năng phục hồi:
def process_payment(order_id, amount, card_token):
validate_order(order_id) # để lỗi validate ném ra tự nhiên, xử lý ở tầng trên
try:
charge_result = payment_gateway.charge(card_token, amount)
except PaymentGatewayError as e:
logger.error(f"Charge failed for order {order_id}: {e}")
raise # không được nuốt — đây là lỗi nghiêm trọng, phải báo cho caller
update_order_status(order_id, "paid") # tiền đã charge thành công, phải chạy
try:
send_receipt_email(order_id)
except EmailError as e:
# không rollback payment chỉ vì gửi mail lỗi — enqueue để retry sau
logger.warning(f"Receipt email failed for order {order_id}, queuing retry: {e}")
enqueue_receipt_retry(order_id)
return {"success": True, "charge_id": charge_result.id}
Prompt để tránh: "Do not wrap the entire function in one try/except. Identify which steps are critical (must fail the whole operation) versus best-effort (should log and continue), and scope error handling per step."
3. Silent Failure Khi Truy Cập Chuỗi Thuộc Tính Lồng Nhau (Chained Null Access)
BEFORE:
function getShippingCity(order) {
return order.customer.address.city;
}
// hoặc phiên bản AI "tự sửa" bằng optional chaining nhưng che giấu lỗi thật
function getShippingCity(order) {
return order?.customer?.address?.city ?? 'Unknown';
}
Dòng đầu tiên throw TypeError: Cannot read properties of undefined khi customer hoặc address là null — bug rõ ràng, dễ bắt. Nhưng dòng thứ hai còn nguy hiểm hơn: AI thường "sửa lỗi" bằng cách rải optional chaining (?.) và giá trị fallback khắp nơi để code không bao giờ crash — nhưng nó biến một lỗi dữ liệu nghiêm trọng (đơn hàng thiếu địa chỉ giao) thành một chuỗi "Unknown" âm thầm trôi vào hệ thống vận chuyển, gây lỗi giao hàng thực tế mà không log nào cảnh báo.
Vì sao AI hay sinh pattern này: khi được yêu cầu "fix this null reference error", AI có xu hướng tìm giải pháp local, tại chỗ (chèn ?. và ??) thay vì đặt câu hỏi ngược "vậy khi nào field này thực sự được phép null, và hệ thống nên làm gì trong trường hợp đó?". Optional chaining là công cụ ngôn ngữ tuyệt vời, nhưng dùng để "dập lửa" báo lỗi thay vì xử lý đúng nghĩa lại tạo ra silent failure.
AFTER — phân biệt "null hợp lệ" và "null là lỗi dữ liệu":
function getShippingCity(order) {
const city = order?.customer?.address?.city;
if (!city) {
throw new DataIntegrityError(
`Order ${order?.id ?? 'unknown'} is missing a valid shipping address`
);
}
return city;
}
Nếu thiếu địa chỉ là tình huống hợp lệ về nghiệp vụ (ví dụ đơn hàng digital không cần ship), thì xử lý rõ ràng bằng nhánh logic riêng — không dùng optional chaining để che giấu:
function getShippingCity(order) {
if (order.deliveryType === 'digital') return null; // hợp lệ, có chủ đích
const city = order.customer?.address?.city;
if (!city) throw new DataIntegrityError(`Missing address for physical order ${order.id}`);
return city;
}
Prompt để tránh: "When fixing a null reference bug, don't just add optional chaining. Tell me whether null is a valid business state here — if it's not, throw a descriptive error instead of silently defaulting."
4. Dùng Sai async/await
BEFORE — race condition kinh điển do quên await, hoặc dùng forEach với async:
async function processOrders(orderIds) {
const results = [];
orderIds.forEach(async (id) => {
const result = await processOrder(id);
results.push(result);
});
return results; // luôn trả về [] — forEach không await callback bên trong!
}
Đây là một trong những lỗi phổ biến nhất mà AI sinh ra khi được yêu cầu "process items in a loop with async calls". Array.prototype.forEach không hỗ trợ async — nó gọi callback nhưng không đợi promise resolve, nên hàm processOrders trả về mảng rỗng ngay lập tức trong khi các order vẫn đang xử lý ngầm, không kiểm soát được, dễ vượt rate limit của service downstream.
Vì sao AI hay sinh pattern này: forEach là pattern lặp mảng quen thuộc nhất trong dữ liệu huấn luyện cho các thao tác đồng bộ. Khi đề bài chuyển sang bất đồng bộ, model vẫn tái sử dụng khuôn mẫu cú pháp quen thuộc (arr.forEach(async ...)) vì nó "trông giống" code hay gặp, dù về ngữ nghĩa hoàn toàn sai.
AFTER — chọn đúng chiến lược tuần tự hay song song có kiểm soát:
// Nếu cần xử lý tuần tự (ví dụ để giữ transaction order):
async function processOrders(orderIds) {
const results = [];
for (const id of orderIds) {
results.push(await processOrder(id));
}
return results;
}
// Nếu cần song song nhưng giới hạn concurrency (tránh làm sập downstream service):
import pLimit from 'p-limit';
async function processOrders(orderIds) {
const limit = pLimit(5);
return Promise.all(orderIds.map((id) => limit(() => processOrder(id))));
}
Prompt để tránh: "Never use forEach with an async callback. Ask me whether these operations need to run sequentially, in full parallel with Promise.all, or with bounded concurrency — and pick accordingly."
5. Lỗi Off-by-One Trong Pagination
BEFORE:
def get_page(items, page_number, page_size=20):
start = page_number * page_size
end = start + page_size
return items[start:end]
Bug ở đây tinh vi: nếu frontend coi "trang 1" là trang đầu tiên (theo trực giác người dùng) nhưng backend code này tính start = page_number * page_size với giả định page bắt đầu từ 0, thì khi frontend gọi page=1, nó nhận về items 20-40 thay vì 0-20 — bỏ sót toàn bộ 20 item đầu tiên. Đây là dạng off-by-one phổ biến nhất: lệch base index (0-based vs 1-based) giữa hai tầng của hệ thống.
Vì sao AI hay sinh pattern này: AI sinh code pagination dựa trên convention phổ biến nhất trong ví dụ nó học (thường là 0-based cho backend), nhưng không có ngữ cảnh về convention mà API contract hoặc frontend team của bạn đã thống nhất. Nó cũng thường quên các trường hợp biên: trang cuối cùng có ít hơn page_size item, hoặc page_number vượt quá tổng số trang.
AFTER — chuẩn hóa rõ ràng và test biên:
def get_page(items, page_number, page_size=20):
if page_number < 1:
raise ValueError("page_number must be 1-based and >= 1")
start = (page_number - 1) * page_size
end = start + page_size
total = len(items)
return {
"items": items[start:end],
"page": page_number,
"total_pages": (total + page_size - 1) // page_size, # ceiling division
"has_next": end < total,
}
Và bộ test biên bắt buộc phải có: trang đầu (page=1), trang cuối có ít item hơn page_size, trang vượt quá tổng số, và page_size không chia hết cho tổng số item.
Prompt để tránh: "This pagination is 1-based at the API boundary. Write unit tests for the first page, last partial page, and out-of-range page before I accept this implementation."
6. Rò Rỉ Tài Nguyên (Resource Leak)
BEFORE:
func processFile(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
data, err := io.ReadAll(file)
if err != nil {
return nil, err // file không bao giờ được Close() nếu ReadAll lỗi!
}
file.Close()
return data, nil
}
Nếu io.ReadAll fail giữa chừng, hàm return sớm mà không gọi file.Close() — file descriptor bị rò rỉ. Với traffic thấp không sao, nhưng chạy production ở scale lớn, service sẽ dần cạn file descriptor và crash với lỗi too many open files — một trong những incident khó truy ngược nhất vì triệu chứng (crash) xuất hiện rất xa nguyên nhân (leak) về mặt thời gian.
Vì sao AI hay sinh pattern này: AI thường sinh code theo trình tự tuyến tính "mở → dùng → đóng" đúng như luồng happy path, và quên rằng return sớm (early return khi lỗi) là điểm thoát bỏ qua bước dọn dẹp. Pattern defer trong Go, finally trong Java/Python, hay using/try-with-resources chính là để giải quyết đúng vấn đề này, nhưng AI không phải lúc nào cũng áp dụng nhất quán, đặc biệt với các resource "ít quen thuộc" hơn (connection pool, mutex lock, file handle, WebSocket).
AFTER — dùng defer/finally ngay sau khi mở resource:
func processFile(path string) ([]byte, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close() // đảm bảo luôn chạy dù return ở đâu bên dưới
data, err := io.ReadAll(file)
if err != nil {
return nil, err
}
return data, nil
}
Áp dụng tương tự với database connection, mutex, hoặc HTTP response body:
async function fetchData(url: string) {
const response = await fetch(url);
try {
return await response.json();
} finally {
// với node-fetch/undici, đảm bảo body stream không bị treo nếu json() throw
if (!response.bodyUsed) await response.body?.cancel();
}
}
Prompt để tránh: "Every resource you open (file, connection, lock, stream) must be closed using defer/finally/try-with-resources immediately after acquisition — not at the end of the function body — so cleanup runs on every exit path including errors."
7. Thiếu Retry Cho Các Thao Tác Idempotent
BEFORE:
async function syncInventory(sku: string, quantity: number) {
const response = await inventoryApi.updateStock(sku, quantity);
if (!response.ok) {
throw new Error(`Failed to sync inventory for ${sku}`);
}
return response;
}
updateStock là thao tác idempotent (gọi lại nhiều lần với cùng tham số cho cùng kết quả cuối) — hoàn hảo để retry khi gặp lỗi mạng thoáng qua (transient network error, timeout, 503). Nhưng code AI sinh ra thường throw ngay lập tức và để tầng gọi (hoặc người dùng) tự xử lý, bỏ lỡ cơ hội tự phục hồi cho một lớp lỗi mà retry đơn giản đã giải quyết được 90% trường hợp trong thực tế (theo kinh nghiệm vận hành hệ thống phân tán phổ biến).
Vì sao AI hay sinh pattern này: retry logic với backoff đúng cách (exponential backoff, jitter, giới hạn số lần thử, phân biệt lỗi nào retry được/không) là logic khá dài và "lệch trọng tâm" so với yêu cầu chính trong prompt (ví dụ "viết hàm sync inventory"), nên model có xu hướng bỏ qua trừ khi được yêu cầu rõ ràng. Ngoài ra, AI không biết được liệu operation có thực sự idempotent hay không nếu không được cho biết — retry mù quáng vào một thao tác không idempotent (như "tạo order mới") sẽ tạo ra bug tệ hơn cả không retry.
AFTER — retry có kiểm soát, chỉ áp dụng cho lỗi transient:
async function syncInventory(sku: string, quantity: number) {
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const response = await inventoryApi.updateStock(sku, quantity);
if (response.ok) return response;
const isTransient = response.status === 503 || response.status === 429 || response.status >= 500;
if (!isTransient || attempt === maxRetries) {
throw new Error(`Failed to sync inventory for ${sku} after ${attempt} attempts (status ${response.status})`);
}
const backoffMs = Math.min(1000 * 2 ** (attempt - 1), 8000) + Math.random() * 200; // exponential backoff + jitter
await sleep(backoffMs);
}
}
Prompt để tránh: "This operation is idempotent. Add retry with exponential backoff and jitter for transient errors (5xx, timeouts, 429) only — do not retry on 4xx client errors, and cap retries at 3 attempts."
Mẹo: In checklist bảy pattern này ra và dán cạnh màn hình khi review PR do AI hỗ trợ viết — trong tháng đầu, cứ mỗi lần review hãy chủ động quét qua từng mục một thay vì đọc code theo cảm tính, cho tới khi việc này trở thành phản xạ tự nhiên.
Dùng Static Analysis Để Bắt Pattern Sớm
Review bằng mắt là cần thiết nhưng không đủ — con người mệt, bỏ sót, và không scale khi volume code do AI sinh ra tăng nhanh. Static analysis nên là tuyến phòng thủ đầu tiên, chạy tự động trong CI hoặc ngay trong editor trước khi PR được tạo.
Với TypeScript/JavaScript, tsc --strict (bật strictNullChecks) là công cụ rẻ nhất nhưng hiệu quả nhất để bắt pattern #3 (chained null access) — compiler sẽ từ chối build nếu bạn truy cập property trên giá trị có thể undefined mà chưa narrow type. ESLint với rule no-floating-promises (từ @typescript-eslint) bắt chính xác pattern #4 (quên await, hoặc dùng async callback trong forEach/map mà không xử lý promise trả về). Rule require-atomic-updates giúp phát hiện race condition khi biến bị cập nhật không nguyên tử giữa các lần await.
Với Python, mypy ở chế độ strict tương đương strictNullChecks, và pylint/ruff có rule bare-except cảnh báo chính xác pattern #2 (bắt Exception trần trụi hoặc dùng except: không chỉ định loại lỗi).
Với Go, golangci-lint gộp nhiều linter hữu ích: errcheck bắt pattern #1 (lỗi return bị bỏ qua không kiểm tra), và bodyclose/rowserrcheck bắt chính xác pattern #6 (quên đóng HTTP response body hoặc sql.Rows).
Với các pattern phức tạp hơn về logic nghiệp vụ (như off-by-one pagination hay thiếu retry cho thao tác idempotent), linter tổng quát khó bắt được vì chúng cần hiểu ngữ cảnh domain. Đây là lúc Semgrep phát huy giá trị — viết custom rule riêng cho codebase của bạn, ví dụ rule chặn mọi lời gọi tới paymentGateway.charge() nằm trong khối try/except Exception rộng, hoặc rule flag mọi hàm mở resource (open(, Connect() không có defer/with liền kề. SonarQube (hoặc SonarCloud) hữu ích ở tầng tổ chức vì nó theo dõi "code smell" theo thời gian và tính điểm technical debt, giúp bạn thấy được liệu tỷ lệ các pattern lỗi này có tăng lên khi việc dùng AI code assistant tăng lên hay không — một tín hiệu quan trọng để điều chỉnh quy trình review.
Điều quan trọng cần nhớ: chính các công cụ AI coding assistant hiện đại (GitHub Copilot, Cursor, Claude Code) ngày càng tích hợp sẵn khả năng chạy linter/type-checker ngay trong vòng lặp sinh code — nếu bạn cấu hình đúng (bật strict mode, thêm custom Semgrep rule vào CI), agent sẽ tự thấy lỗi do chính nó gây ra và tự sửa trước khi bạn kịp review, biến static analysis từ "lưới an toàn sau cùng" thành một phần của vòng lặp sinh-kiểm tra-sửa ngay từ đầu.
Mẹo: Thêm một bước bắt buộc trong CI pipeline: chạy
tsc --noEmit(hoặcmypy/golangci-linttương ứng) và ESLint/Semgrep custom rules trên mọi PR, kể cả PR nhỏ — vì code do AI sinh có xu hướng "trông sạch" nên dễ được approve nhanh nếu không có gate tự động.
Thực Hành: Chẩn Đoán và Sửa Các Failure Pattern Do AI Sinh Ra
Giả sử bạn nhận một PR từ đồng nghiệp, được sinh phần lớn bởi Cursor, implement chức năng "lấy danh sách đơn hàng chưa thanh toán của một customer, theo trang, và gửi email nhắc thanh toán". Đây là đoạn code cần review:
async function getUnpaidOrdersAndNotify(customerId: string, page: number, pageSize: number) {
const allOrders = await orderService.getOrdersByCustomer(customerId);
const unpaid = allOrders.filter(o => o.status === 'unpaid');
const start = page * pageSize;
const paged = unpaid.slice(start, start + pageSize);
try {
paged.forEach(async (order) => {
const email = order.customer.contact.email;
await emailService.sendReminder(email, order.id);
await orderService.markReminderSent(order.id);
});
} catch (e) {
console.log('error sending reminders');
}
return paged;
}
Đây là bài tập chẩn đoán từng bước như bạn sẽ làm thật với một AI coding assistant.
Bước 1 — Đưa code cho AI với prompt chẩn đoán có cấu trúc, không chỉ hỏi chung chung "review giúp tôi". Prompt thực tế nên gõ:
Review this function for the seven common AI-generated failure patterns:
missing error propagation, overly broad try/catch, silent null access,
incorrect async/await usage, off-by-one pagination, resource leaks, and
missing retry on idempotent operations.
For each issue found, tell me: (1) which pattern it matches, (2) the exact
line, (3) the concrete failure scenario that would trigger it in production,
and (4) whether it's silent (no error surfaced) or loud (throws/crashes).
Here is the function:
[paste code]
Bước 2 — Đọc kỹ output và tự xác nhận lại, không tin tuyệt đối. Một response hợp lý từ AI sẽ chỉ ra:
- Pattern #4 (async/await sai):
paged.forEach(async (order) => {...})— callback async không được await, nên hàmgetUnpaidOrdersAndNotifytrả vềpagedngay lập tức trong khi email vẫn đang gửi ngầm, không đồng bộ, và caller không biết việc gửi nhắc nhở đã hoàn tất hay chưa. - Pattern #2 (try/catch quá rộng) kết hợp pattern #1 (thiếu error propagation): khối
try/catchbao cả vòng lặp, nhưng vìforEachkhông await, catch này thực chất không bao giờ bắt được lỗi từ bên trong callback — lỗi async throw ra sẽ trở thành unhandled promise rejection, cònconsole.logthì không giúp ích gì cho việc điều tra sau này, và caller hoàn toàn không biết có bao nhiêu email gửi thất bại. - Pattern #3 (chained null access tiềm ẩn):
order.customer.contact.emailkhông có optional chaining hay kiểm tra — nếu một order cócustomernull (dữ liệu cũ, hoặc guest checkout không lưu contact), toàn bộ iteration đó throw exception không được catch đúng cách như đã phân tích ở trên. - Pattern #5 (off-by-one pagination):
start = page * pageSizegiả địnhpagebắt đầu từ 0, cần xác nhận với API contract xem frontend gửipage0-based hay 1-based. - Thiếu pattern #7 (không có retry):
emailService.sendReminderrất có thể là thao tác idempotent-đủ-an-toàn (gửi lại email nhắc nhở không gây hại), nhưng không có retry cho lỗi mạng thoáng qua.
Bước 3 — Yêu cầu AI sửa với ràng buộc rõ ràng, tránh nó chỉ vá bề mặt. Prompt tiếp theo:
Fix all issues found above with these constraints:
- Process reminder emails sequentially with bounded concurrency (max 3 at once),
since sendReminder calls an external SMTP-backed service that rate-limits us.
- Each order's reminder send + markReminderSent must be tracked independently:
if one order fails, others should still be attempted, and return a summary
of which orders succeeded/failed instead of a generic log line.
- Throw a clear error if customer.contact.email is missing instead of crashing
with a null reference — treat missing email as a data integrity issue to report,
not something to skip silently.
- Confirm page is 1-based per our API contract before computing the pagination
offset, and add a guard against page < 1.
- Add retry with exponential backoff for sendReminder on transient failures only.
Bước 4 — Kết quả sau khi sửa, có thể trông như sau:
import pLimit from 'p-limit';
async function getUnpaidOrdersAndNotify(customerId: string, page: number, pageSize: number) {
if (page < 1) throw new Error('page must be 1-based and >= 1');
const allOrders = await orderService.getOrdersByCustomer(customerId);
const unpaid = allOrders.filter(o => o.status === 'unpaid');
const start = (page - 1) * pageSize;
const paged = unpaid.slice(start, start + pageSize);
const limit = pLimit(3);
const results = await Promise.all(
paged.map((order) =>
limit(async () => {
try {
const email = order.customer?.contact?.email;
if (!email) {
throw new DataIntegrityError(`Order ${order.id} has no contact email`);
}
await sendReminderWithRetry(email, order.id);
await orderService.markReminderSent(order.id);
return { orderId: order.id, status: 'sent' as const };
} catch (err) {
logger.error(`Reminder failed for order ${order.id}`, err);
return { orderId: order.id, status: 'failed' as const, reason: String(err) };
}
})
)
);
return { orders: paged, reminderResults: results };
}
async function sendReminderWithRetry(email: string, orderId: string, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await emailService.sendReminder(email, orderId);
} catch (err) {
const isTransient = err instanceof TransientEmailError;
if (!isTransient || attempt === maxRetries) throw err;
await sleep(Math.min(500 * 2 ** (attempt - 1), 4000));
}
}
}
Bước 5 — Chạy static analysis và test trước khi merge. tsc --strict xác nhận không còn chỗ nào truy cập property trên giá trị undefined mà chưa narrow. ESLint no-floating-promises xác nhận không còn async callback nào bị bỏ quên trong forEach/map. Viết unit test cho các trường hợp biên: order thiếu contact email, sendReminder fail 2 lần rồi thành công ở lần 3, và page=1/page vượt quá tổng số trang.
Bài tập này minh họa điều quan trọng nhất: một đoạn code AI sinh ra tưởng chừng gọn gàng có thể mang 4-5 failure pattern chồng lên nhau, và chúng thường tương tác với nhau theo cách làm cho bug khó phát hiện hơn (như try/catch không bao giờ kích hoạt vì forEach không await) — nên kỹ năng chẩn đoán có hệ thống, dùng đúng prompt cấu trúc, quan trọng hơn nhiều so với việc đọc lướt và tin vào cảm giác "trông ổn".
Mẹo: Khi yêu cầu AI sửa lỗi, luôn liệt kê ràng buộc nghiệp vụ cụ thể (concurrency limit, thao tác nào idempotent, page 0-based hay 1-based) trong cùng một prompt — nếu không, AI sẽ đoán và có thể đoán sai theo đúng cách nó đã sai lúc đầu.
Những điểm chính
- Lỗi trong code do AI sinh ra có tính hệ thống, không ngẫu nhiên — bắt nguồn từ thiên lệch dữ liệu huấn luyện, thiếu mô phỏng runtime, và việc tối ưu cho "trông hợp lý" hơn "chạy đúng".
- Bảy pattern cần quét mỗi lần review: thiếu error propagation, try/catch quá rộng, silent failure trên chained null access, dùng sai async/await (đặc biệt
forEachvới callback async), off-by-one pagination, resource leak khi return sớm, và thiếu retry cho thao tác idempotent. - Mỗi pattern có "chữ ký" prompt riêng để phòng tránh — nêu rõ ràng buộc (idempotent hay không, concurrency limit, convention 0-based/1-based) thay vì để AI tự đoán.
- Static analysis (
tsc --strict, ESLintno-floating-promises,mypy,golangci-lintvớierrcheck/bodyclose, Semgrep custom rule, SonarQube) nên là tuyến phòng thủ tự động trong CI, không chỉ dựa vào review bằng mắt. - Các pattern thường xuất hiện chồng lên nhau và tương tác theo cách che giấu lẫn nhau (ví dụ try/catch không bao giờ bắt được lỗi vì thiếu await) — luôn yêu cầu AI liệt kê rõ pattern, dòng, và kịch bản lỗi cụ thể khi chẩn đoán, đừng chỉ hỏi "review giúp tôi".
- Biến checklist bảy pattern này thành phản xạ tự động khi review PR do AI hỗ trợ viết, cho tới khi việc quét qua từng mục trở thành bản năng nghề nghiệp.