# Error Handling Basics: Why Apps Break Silently

> Error handling explained simply: why apps fail silently, what to do with errors on the frontend and backend, and what to show users versus what to log.

Published: 2026-10-20 · Source: https://subxcat.com/articles/error-handling-basics · Author: SUBXCAT

Error handling is a decision made in advance about what to do when something doesn't work: a request didn't arrive, the database didn't respond, a person typed the wrong thing. An app with good error handling tells the user what happened and what to do next, and tells the developer where and why.

In AI-written code that decision is often missing. The assistant writes the happy path, where everything succeeds. The result works in the demo and sometimes simply does nothing for a real person. The button was clicked, the spinner spins forever, the database is empty, and nobody, including you, knows about it.

## Why apps break silently

Three common sources of silence:

- **The error was caught and thrown away.** An empty `catch {}`, or `catch (e) { console.log(e) }` in the browser. Technically handled, actually hidden.
- **The error was never expected.** An API call with no check on the response: the server returned `500`, the code reads the data as if all is well and shows an empty list.
- **The error happened where nobody looks.** A background job, a webhook, a scheduled function. It failed and left no trace.

Start by searching the code for `catch`, `.catch(` and `except`. Every place that quietly swallows an error is a candidate for fixing.

## Two audiences: the user and the log

Every error has two readers, and they need different things.

**The user** needs to understand what happened on their side and what to do: "Couldn't save, check your connection and try again", "This email is already registered, sign in or reset your password". No technical detail, no stack trace, no table names. System internals in an error message are also a hint for an attacker.

**The log** needs everything required to investigate later: which operation, which user (an ID, not an email), what inputs, the full error text and the stack. But no passwords, tokens or card numbers.

A good practice is to send the same event to both places: a short, clear message for the user and a detailed log entry, linked by a shared ID. Then a support email saying "I got an error, code A7F3" leads you to a specific log line.

## Error handling on the frontend

In the browser, the job is not to leave the person guessing.

- **Every request has three states**: loading, success and error. If a component only handles the first two, the third will still happen, just without any interface.
- **Check the server's response.** `fetch` doesn't treat a `404` or `500` as an error; you have to look at `response.ok` yourself.
- **Don't lose what was typed.** If a form submission fails, the data should stay in the fields. Making someone refill a long form is a reliable way to lose them.
- **Add a safety net.** In React that's an error boundary: a component that shows "something went wrong" with a reload button instead of a blank white screen.
- **Send browser errors to yourself.** Without an error-tracking service you won't find out that everything crashes for users on a particular browser.

## Error handling on the backend

On the server, the job is not to lie about the result and not to lose information.

- **Correct status codes.** Bad input is `400`, no access is `403`, not found is `404`, broken on our side is `500`. A `200` with "error" in the body breaks all the logic on the client.
- **Validate at the door.** The earlier bad data is rejected, the fewer strange errors appear deeper down.
- **External services fail.** Put timeouts on calls to them and decide in advance what happens on failure: retry, queue it, or honestly return an error.
- **Don't do half an operation.** If you need to deduct a balance and create an order, that's one transaction: both steps or neither.

A minimal handler pattern looks roughly like this:

```js
try {
  const order = await createOrder(input);
  return res.status(201).json(order);
} catch (err) {
  const id = crypto.randomUUID();
  logger.error({ id, userId: user.id, err }, "createOrder failed");
  return res.status(500).json({ error: "Couldn't place the order", id });
}
```

## What not to do

- Show the user the raw exception text.
- Catch every error and carry on as if nothing happened.
- Retry endlessly a request that fails every time, especially to a paid API.
- Log the entire request body, password included.

## In short

- Error handling is a decision made in advance about what happens when something fails.
- The most dangerous error is a swallowed one: an empty `catch` is worse than a crash.
- The user gets a clear message and a next step, the log gets details without secrets, and a shared ID links them.
- On the frontend, every request has an error state and typed data is never lost.
- On the backend: honest status codes, timeouts on external services, and transactions.

If your project has dozens of empty `catch` blocks, SUBXCAT can work through them and set up error tracking so silent failures become visible.
