If an AI wrote most of your app, there is a kind of bug you probably have and cannot see. Nothing crashes. No alert fires. A user clicks a button, nothing happens, and they leave.

I am not against generated code. I ship plenty of it. But it is written to make the thing in front of you work, and the quickest way to make something work is to stop it from failing loudly.

What a swallowed error looks like

Here is the shape to look for. This example is hypothetical, but you will recognise it.

async function saveProfile(profile) {
  try {
    await db.profiles.update(profile);
  } catch (err) {
    console.log(err);
  }
  return { ok: true };
}

The update can fail for a dozen reasons. The function returns { ok: true } for all of them. The UI shows a green tick. The only record is a console.log on a server nobody is watching.

A few close relatives:

  • The empty catch. catch {} with nothing inside.
  • The friendly default. catch { return [] }, so a failed query looks exactly like an empty list.
  • The unchecked response. await fetch(url) with no look at response.ok, so a 500 from a payment API is treated as success.
  • The floating promise. An async call with no await and no .catch, in a place where the rejection goes nowhere useful.
  • The optional chain that hides a bug. user?.plan?.limits?.seats returning undefined when the real problem is that plan never loaded.

None of these are exotic. They pass review because the code runs. They pass the demo because the happy path works.

Why generated code does this

An assistant that sees a stack trace is being asked to make the stack trace go away. Wrapping the line in a try/catch does that. So does returning a default. Both are reasonable answers to “fix this error” and bad answers to “make this work”.

You also tend to read generated code less closely than code you typed. A catch block that swallows an error is three lines in a two hundred line diff.

What to log

You do not need to log everything. You need to log the moments where the app decided to carry on after something went wrong.

Every catch block. If you catch it, log it, with the stack. Then decide whether to rethrow.

Every call that leaves your process. Payment, email, auth, storage, any third-party API. Log the failure with the status code and which operation it was.

Every fallback. If the code falls back to a default, a cache, or an empty state, log that it did, at warn.

Enough context to find the user. A user ID, a request or trace ID, and the route or job name. Without those, an error is a fact with no story.

Not secrets. No passwords, tokens, card numbers or full request bodies.

Here is the same function with the failure made visible, using the Auralogs JavaScript SDK. Any structured logger works the same way.

import { auralogs } from "auralogs-sdk";

async function saveProfile(profile) {
  try {
    await db.profiles.update(profile);
    return { ok: true };
  } catch (err) {
    auralogs.error(
      "Profile update failed",
      { userId: profile.userId, operation: "saveProfile" },
      err.stack,
    );
    return { ok: false };
  }
}

Two changes. The error is logged at error with a stack and a user ID, and the function stops lying about the result.

A short checklist

Run this against your own repo. It takes less time than it looks.

  1. Search for catch and read every block. Anything empty, or that only calls console.log, gets a real log and a decision about rethrowing.
  2. Search for fetch( and check each one looks at response.ok or the status.
  3. Search for return [], return null and return {} inside catch blocks. Each is a failure dressed up as an empty result.
  4. Turn on your linter’s rule for unhandled promises if it has one.
  5. Make sure uncaught exceptions and unhandled rejections go somewhere you will see, not only to a terminal.
  6. Add a user ID and a request ID to every error log.
  7. Pick one error on purpose, trigger it in production, and confirm you can find it within a minute. If you cannot, the logging is not done.
  8. Tell your coding assistant, in its project instructions, to never swallow an error without logging it. Then check the diff anyway.

Step 7 is the one people skip. A log pipeline you have never tested is the same as no log pipeline.

Where Auralogs fits

Auralogs is the logger I built for this. Calling init() captures uncaught exceptions and unhandled promise rejections automatically, so the errors nobody caught still arrive. Errors you log yourself arrive with the metadata you attach.

If you connect a GitHub repo and add your own Anthropic or OpenAI key, Auralogs analyses errors against your source and, when it is confident in a fix, opens a pull request for you to review. It never merges anything itself. Without the repo and the key it is still a structured logger with search and alerts, which is the part this post is about.

The free plan covers 500,000 logs a month with no credit card.

Start free at auralogs.ai