Fashion e-commerce runs paid orders through Centra, the commerce platform where checkout, tax, and shipping lines live. When payment clears, a TypeScript queue worker listens for Centra events and pushes an outbound order into the warehouse system so pickers can ship. Ops and engineering watch sync logs when something stalls. The warehouse only sees orders that export cleanly.
One night a poison order sat in front of everything else. Centra threw Centra order 88421 is missing a complete shipping address. The shopper had paid, but city was empty in Centra. No retry would invent a city. The warehouse correctly never received it. Our worker still returned HTTP 500, treated the failure like a transient sync error (timeouts, rate limits, upstream 5xx that might succeed later), and the broker redelivered the same message for roughly eighty minutes. Stock updates and other exports waited behind one bad payload.
A transient sync error is worth retrying. A permanent sync error is bad data: missing shipping option URI, impossible SKU mapping, or an incomplete address. It will fail the same way until a human fixes the order in Centra. I had already classified some Centra messages as permanent. Incomplete address was not on the list, so it fell through to the default path that rethrows and asks the queue to try again.
Classify, log, ack
The fix is boring and reusable. Extend the shared permanentErrorPattern list with missing a complete shipping address. When the classifier matches, log permanent_error with errorClass: permanent, record the Centra order id, and acknowledge the Centra event without rethrowing. The queue drains. Other entities keep processing. Ops still gets a loud log line to fix the address in Centra; the worker stops pretending the next attempt will magically fill in city.
const permanentErrorPattern = [
/missing shipping option uri/i,
/missing a complete shipping address/i,
];
function classifySyncError(error: unknown): 'permanent' | 'transient' {
const message = error instanceof Error ? error.message : String(error);
if (permanentErrorPattern.some((re) => re.test(message))) {
return 'permanent';
}
return 'transient';
}
async function handleOrderExport(event: CentraEvent) {
try {
await exportOrderToWarehouse(event.orderId);
} catch (error) {
if (classifySyncError(error) === 'permanent') {
logger.warn('permanent_error', { errorClass: 'permanent', orderId: event.orderId, message: String(error) });
return ack(event);
}
throw error;
}
}Unit tests cover the regex match and the logging path so nobody regresses incomplete address back into transient land. That is the same discipline I use for other permanent Centra strings: one helper, one list, tests on the classifier not on production orders.
Try the demo
Toggle Naive (transient) versus Fixed (permanent), then press Deliver order event. On Naive, each click bumps the redelivery counter and leaves the worker on HTTP 500 while the rows below stay blocked. On Fixed, one deliver logs permanent_error, acks the poison event, and the next queue item moves. Reset if you want a clean run.
Centra order export queue
- 1. Order export (incomplete address)waiting
- 2. Stock delta syncwaiting
- 3. Return label webhookwaiting
HTTP: Idle
Redeliveries on poison event: 0
Pick Naive or Fixed, then deliver the poison order event.
Any thrown Error looks retryable unless you teach the classifier otherwise. Incomplete address data is not flakiness. Ack it, log it, and let the rest of the queue breathe.
Happy coding! Sander