Your AI-written app is failing silently. Here is how to find out before your users do.
Generated code is good at making the demo run and bad at telling you when production does not. What swallowed errors look like, what to log, and a short checklist.
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 atresponse.ok, so a 500 from a payment API is treated as success. - The floating promise. An async call with no
awaitand no.catch, in a place where the rejection goes nowhere useful. - The optional chain that hides a bug.
user?.plan?.limits?.seatsreturningundefinedwhen the real problem is thatplannever 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.
- Search for
catchand read every block. Anything empty, or that only callsconsole.log, gets a real log and a decision about rethrowing. - Search for
fetch(and check each one looks atresponse.okor the status. - Search for
return [],return nullandreturn {}inside catch blocks. Each is a failure dressed up as an empty result. - Turn on your linter’s rule for unhandled promises if it has one.
- Make sure uncaught exceptions and unhandled rejections go somewhere you will see, not only to a terminal.
- Add a user ID and a request ID to every error log.
- 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.
- 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.