Engineering Note: Fixing Silent Failures in Screening and GDPR Deletion (v0.1.7)
Engineering Note: Fixing Silent Failures in Screening and GDPR Deletion (v0.1.7)
Control: ERR-08 — No Unhandled Promise Rejections
Affected files:src/app/api/people/route.ts,src/app/api/gdpr-delete/route.ts
Background
Silent catch blocks are one of the most dangerous patterns in a compliance-critical application. When an error is swallowed without capture, it creates a gap between what the system believes has happened and what actually happened — and in sanctions screening or GDPR processing, that gap carries real regulatory risk.
v0.1.7 closes two such gaps identified under control ERR-08.
Problem 1: Screening Block in people/route.ts
What was happening
The POST handler in people/route.ts performs a sanctions screening check after a person record is created. The screening block — which writes match records and updates person status to the database — was wrapped in a bare catch:
// Before (v0.1.6 and earlier)
try {
// ... insert matches, update person status
} catch {
console.error('Screening failed');
}
If the database was unavailable, a constraint was violated, or a network timeout occurred during this block, the error would print to the server console and vanish. The API caller would receive a success response. No alert would fire. No record would exist that screening had failed.
The fix
// After (v0.1.7)
try {
// ... insert matches, update person status
} catch (screeningError) {
captureError(screeningError, {
domain: 'screening',
metadata: { personId: person.id },
});
}
The error is now captured with structured context — the domain tag routes it to the correct alerting channel, and personId makes it immediately actionable for on-call responders. Critically, the error is still not re-thrown: the POST response is not disrupted, matching the original intent of the silent catch, but the failure is no longer invisible.
Problem 2: Per-Person Loop in gdpr-delete/route.ts
What was happening
The GDPR deletion endpoint iterates over a list of person records and anonymises each one. Before this fix, the loop had no per-row error handling:
// Before (v0.1.6 and earlier)
for (const person of people) {
await anonymisePerson(person.id); // if this throws, loop stops
}
A single row failure — a locked record, a constraint violation, a transient DB error — would throw out of the loop. All records after the failing row would remain unanonymised. The endpoint would return an error, but no information would be available about which records had been processed and which had not. The dataset would be left in a partially-anonymised state.
For GDPR right-to-erasure requests, partial anonymisation is a compliance failure.
The fix
Each iteration is now individually guarded:
// After (v0.1.7)
const results = { succeeded: [], failed: [] };
for (const person of people) {
try {
await anonymisePerson(person.id);
results.succeeded.push(person.id);
} catch (rowError) {
captureError(rowError, {
domain: 'gdpr-delete',
metadata: { personId: person.id },
});
results.failed.push(person.id);
}
}
Failures on individual rows are captured with context and collected into a partial results object. Processing continues for all remaining records. Callers receive a structured response indicating which records succeeded and which did not, enabling targeted retry or manual remediation.
Why This Matters for Compliance Platforms
Silent errors in a sanctions screening platform have a specific failure mode: the system appears healthy while silently failing to record matches or update statuses. An operator relying on the dashboard would see no alerts, no failures — just missing data.
The same applies to GDPR deletion: a partially-anonymised dataset satisfies neither the letter nor the spirit of an erasure request, and without error capture there would be no audit trail showing that something had gone wrong.
ERR-08 addresses both by making failures observable without making the API surface brittle.
Upgrade
This change is fully backwards-compatible. No database migrations, configuration changes, or client updates are required. Deploy as a standard patch release.