# Silent Failures: Why Your App Says “Success” When the Request Failed

You fix one thing, two others break, and last week's bug returns. The loop runs because nothing is telling you the truth — and the most common failure is not an error message but silence.

URL: https://www.back4app.com/blog/silent-failures-app-says-success-request-failed
Published: 2026-11-12 · Tested: 2026-10-27
Tested on: Local: Node 22.21.1 on macOS, arm64 · One order flow written two ways, one provider with a ledger, no dependencies · Back4app, free plan
Publisher: Back4app Engineering

You fix one thing, two others break, and a bug you killed last week comes back. Nobody has solved this, and no platform can stop a model from going round in circles.

But a loop needs something to converge on, and that is the part you control. If a broken feature reports success, you cannot tell whether the last round of changes helped — so you ask for another round against the same false picture.

The most common failure we hit was not an error at all. It was silence. So we measured how much a generated app's error handling actually hides.

## Watch it instead

The doom loop is number 4 in our countdown of what actually goes wrong for people building with AI.

## What did the app say, and what actually happened?

Two completely different stories, from the same run:

![Five integrations compared: the app in its generated shape reported all five steps succeeded, the same five written honestly reported all five failed, and the provider's ledger shows none of the five actually happened](/blog/blog-assets/the-failures-nobody-saw/what-the-app-said.png)

The right-hand column is not the app's opinion. It is the provider's own ledger — a count of receipts sent, inventory commits made and cards charged, kept on the other side of the network where the app cannot affect it.

Five features. Five green ticks. Nothing happened.

## Why didn't anything get logged?

Because there was nothing to log. This is the shape the code came in:

```js
try {
  await fetch(`${BASE}/v1/messages`, { method: 'POST', body: JSON.stringify({ to: order.email }) });
  return { ok: true };
} catch (e) { console.log('could not send receipt'); return { ok: true }; }
```

Read it as the computer does. The request is made. A response comes back. Nothing looks at it. `{ ok: true }` is returned because that is the only thing this function can return.

The `catch` is not even reached — and when it is, it returns `{ ok: true }` anyway.

## Does try/catch catch an HTTP error at all?

No, and that is the measurement worth carrying away from this article. We put each failure through a bare call and recorded whether it threw:

```text
the failure             does it throw?  what the code gets back
endpoint removed (410)  no              returned a Response
field renamed (200)     no              returned NaN
key rotated (401)       no              returned a Response
never answers           YES             TimeoutError
ingest down (500)       no              returned a Response
```

`fetch` rejects only when the request never completed — DNS failure, connection refused, an abort. A server replying "this endpoint is gone" is a completed request, and therefore a success.

The quietest of the five is the renamed field. Summing a field that no longer exists gives `NaN`, and `NaN > 0` is false, so the next step is skipped. Valid 200, valid JSON, no error anywhere, and the inventory never syncs.

## What did the honest version say?

Exactly what was wrong, in five lines, including the remedy:

```text
sendReceipt: HTTP 410 — POST /v1/messages was removed in v3. Use POST /v3/email with {to, template}.
syncInventory: inventory total is not a number — a field name changed
chargeCard: HTTP 401 — This key was rotated on 2026-09-30.
fetchRates: no answer within 3000 ms
logAnalytics: HTTP 500 — ingest_unavailable
```

Look at the first line again. The provider said the endpoint had moved and named its replacement. That body arrived in the silent run too — identical bytes, same response — and was discarded without being read.

## What does this have to do with the loop?

It is the loop's fuel. Round one asks for a fix, round two reports success, and nothing has changed — so round three is asked against the same false picture, and the model has no more information than it had at the start.

Making failures visible does not stop a model from going round in circles. What it does is let you see which round actually worked, which turns an argument with a machine into a sequence of checkable steps.

The other half of surviving the loop is being able to step back cleanly, which we measured separately in [undoing what an AI assistant broke](https://www.back4app.com/blog/undo-ai-assistant-code-changes). Visible failures tell you *that* a round was bad; versioning lets you leave it behind in one command.

## What should you change in the code?

Four things, and they are small. Check `response.ok` on every call, and put the provider's status and message into the error you raise — that message is often the entire fix.

Give every outbound call a timeout, because a hang is not an exception and will not be caught by anything. And validate the shape of what came back, not just that something came back, which is the only thing that would have caught the renamed field.

## What should you do first?

Search your code for `catch` and read every one out loud. A `catch` that logs nothing is a decision, made once, to never find out.

Then take your most important integration and ask what would look different if it silently did nothing. If the honest answer is "nothing visible", then you do not currently know whether it works — and neither does whatever you are asking to fix it.

[The lab is in the repo](https://github.com/templates-back4app/silent-lab) — five integrations, two ways of writing them, and a provider that keeps score independently of what the app believes.
