The Agent Added a 'Safe' Fallback That Quietly Returned Empty Data
Quick answer: A nullish fallback like data ?? [] is a legitimate way to handle a value that is genuinely optional, but an AI agent often reaches for it to silence a type error or an "cannot read property of undefined" crash. The result is a silent failure: when the upstream call actually breaks, the UI renders an empty list instead of surfacing the error, and the bug hides in production. Treat the fallback as a decision about what the failure mode should be, not as a syntax fix.
How the empty fallback gets introduced
Here is the typical chain. The agent is wiring a profile page to a service that returns a list of projects. The OpenAPI contract marks the field as present, but the generated type says it can be undefined because the response uses an optional wrapper. The agent sees a type error on projects.map, and the fastest way to make the checker green is to default it.
// Before: honest, but the type checker complains
const projects = await client.getProjects(userId);
return projects.map((p) => ({ id: p.id, name: p.name }));
// After: checker is green, and the crash is gone
const projects = (await client.getProjects(userId)) ?? [];
return projects.map((p) => ({ id: p.name }));
On the surface this is reasonable. The problem is that ?? [] collapses two very different situations into one branch. A field that is legitimately absent and a network call that threw and returned nothing now look identical. In most generated clients, a failed HTTP request does not return undefined at all; it throws, and a separate catch that returns an empty array is usually where the real masking happens.
Why an empty screen is worse than a loud error
An error that throws is cheap to diagnose. You get a stack, a status code, and a failed request in the network panel. An empty list gives you none of those. The user sees "no projects yet," assumes they simply have not created any, and never reports it. The team's error tracker stays quiet because nothing was thrown. This is the precise failure mode that makes agent-written code feel trustworthy in a demo and unreliable months later.
| Behavior | Loud failure | Empty fallback |
|---|---|---|
| What the user sees | An error state or retry prompt | A blank section that looks intentional |
| Error tracker | Receives the exception | Nothing is reported |
| Time to detect | Immediate | Weeks, often via a support ticket |
| Recovery | User can retry | User must refresh and hope |
Where the fallback is actually correct
Do not read this as a ban on ?? []. The operator is correct when the empty value is an expected, documented state. A search endpoint that returns an empty array for zero matches is a real answer, not a failure. A feature flag payload that omits a new field for older clients is also a legitimate optional value. The distinction is whether the empty branch represents a valid domain state or a swallowed infrastructure error.
A safer pattern that keeps both cases honest
Separate the optional-field handling from the error handling. Let the request throw on transport and status failures, and only default the specific field that the contract says can be absent.
async function loadProjects(userId) {
// A 4xx/5xx or network error rejects and reaches the caller's catch.
const response = await client.getProjects(userId);
// Only the genuinely optional field gets a default.
const projects = response.projects ?? [];
return projects.map((p) => ({ id: p.id, name: p.name }));
}
// At the boundary, decide the real failure mode instead of hiding it.
try {
const projects = await loadProjects(userId);
render(projects);
} catch (err) {
reportError(err);
renderErrorState({ message: 'We could not load your projects.', retry: () => render(userId) });
}
If the upstream genuinely returns null to mean "not found" rather than an error, encode that explicitly with a named branch so a reviewer can see the intent. A comment and a named constant beat an operator that disappears into a long expression.
const NOT_FOUND = null;
const projects = response === NOT_FOUND ? [] : response.projects;
How to catch it in review
When you review an agent-generated diff, search for the spots where a fallback was added in the same commit as the code that consumes the value. Ask one question: is this an optional domain field, or is the agent papering over a call that can fail? If the diff includes a catch that returns [], {}, or an empty string, that is the highest-risk line in the change, because it converts every exception into a successful-looking response. Require the agent to add a test for the failure branch, not just the empty-list branch.
Frequently asked questions
Is ?? [] always a bug when an AI adds it?
No. It is correct for values the contract declares optional and for endpoints where an empty result is a meaningful answer. It is a bug when it is used to make a failing request look successful or to silence a type error without checking why the value can be undefined.
What about the optional chaining I added to avoid a crash?
Optional chaining and nullish defaults are useful for defensive rendering, but they should sit at the boundary of a well-defined optional value. If you find yourself chaining them several levels deep to navigate a response the API promised would be shaped differently, that is a sign the type or the request is wrong, not a reason to keep adding operators.
Should the agent ever catch and return an empty array?
Only as a deliberate, documented product decision, and usually not. A catch block that returns empty data hides the error from both the user and monitoring. Prefer rethrowing, reporting to your error tracker, and rendering an explicit retry state.
How do I test the difference between empty and broken?
Write two cases. Mock the client to resolve with an empty array and assert the empty state renders, then mock it to reject with a network error and assert the error state and retry handler appear. A test suite that only covers the empty branch is exactly how this defect survives.
This is part of the The AI Coding Agent Guide — start there for the full picture of how agent-written code fails in practice and how to review it.