{"slug": "three-bugs-that-reported-success-what-exit-0-doesn-t-tell-you", "title": "Three bugs that reported success: what \"exit 0\" doesn't tell you", "summary": "A developer building a paid business-verification API for AI agents documented three bugs that each reported success instead of failure: a Korean government debarment API that returned HTTP 200 with an empty result set because the inqryDiv parameter was set to 1 instead of 2, a Cloud Run scheduled job that exited 0 without running because a hand-built import.meta.url guard produced four slashes on Linux, and a Git Bash terminal that mangled UTF-8 in Korean company-name queries before they left the machine. The developer argues that green checkmarks and exit codes only prove a container started and stopped without crashing, not that the intended work happened.", "body_md": "Last week I wrote about shipping a paid API for AI agents and getting two real calls in a week. The follow-up was supposed to be about fixing the product. It turned into something else, because in the process of fixing it I hit three bugs in a row that had one thing in common:\n\nNone of them reported an error. All three looked like success.\n\nThat turns out to be the failure mode that costs the most time, and I don't see it discussed much, so here they are.\n\n**Bug 1: the government API that answers \"zero\" instead of \"wrong\"**\n\nI was adding debarment records — public sanctions against companies barred from public procurement — to my business verification API. The endpoint took a parameter called inqryDiv. I passed 1.\n\nIt returned HTTP 200 with an empty result set. No error, no warning, no hint. The obvious reading: this company has no sanctions on record.\n\nThe correct value was 2. With 1, the API answers a question nobody asked and returns nothing, forever, for every company.\n\nIf I'd shipped that, my API would have told users \"no sanctions found\" for every single company in Korea, with a straight face and a 200 status code. That's not a broken feature. That's a feature that confidently lies.\n\nWhat saved me: I happened to test against a company I knew was sanctioned. If I'd tested with a random company — which would also have returned zero, correctly — I'd never have caught it.\n\n**\n\nBug 2: the Cloud Run job that ran nothing and exited 0**\n\nI built a scheduled job to enrich my index with registration numbers. Registered it with Cloud Scheduler. Ran it once manually before trusting the schedule.\n\nExit code 0. Duration: under a second. Cloud Run console: green check.\n\nNothing had happened.\n\nThe entry point had a guard to detect whether the module was being run directly, and it built the comparison URL by hand:\n\njs\n\n// Works on my machine. Not on Linux.\n\nif (import.meta.url === `file:///${process.argv[1]}`) { main(); }\n\nOn Windows, process.argv[1] looks like C:\\path\\to\\file.js, so prefixing file:/// is right. On Linux it's already /app/dist/job.js, so you get file:////app/dist/job.js — four slashes — which never matches. The guard silently evaluated false, main() never ran, the process exited cleanly, and the platform reported success.\n\nThe fix is one line (pathToFileURL(process.argv[1]).href), but that's not the interesting part. The interesting part is that a green checkmark in a scheduler dashboard is not evidence that your code ran. It's evidence that a container started and stopped without crashing. Those are very different claims.\n\nIf I had trusted the schedule instead of running it once by hand, I would have seen green checkmarks every morning for a week while the index sat untouched, and I'd have gone looking for the problem in the API, the credentials, the data — anywhere but the one line that decides whether the program starts at all.\n\n**Bug 3: the query that returned nothing because of my terminal**\n\nWith the job fixed, I tested search with Korean company names over curl in Git Bash. Zero results. Every time. English names worked fine.\n\nI was minutes away from digging into the normalization pipeline — Korean text has its own encoding pitfalls, so a bug there was entirely plausible — when I ran the identical query from a local Node script instead. It worked.\n\nThe server was fine. Git Bash was mangling the UTF-8 in the URL before it ever left my machine.\n\nThis one cost the least time but taught the sharpest lesson: when a test fails, the test environment is a suspect too. I'd been treating my terminal as a neutral observer. It wasn't.\n\n**The pattern**\n\nThree different layers — a government API, a container platform, a shell — and the same shape each time. Something went wrong, and every signal I had available said things were fine.\n\nWhat they have in common is that each one answered a different question than the one I was asking.\n\nI asked \"does this company have sanctions?\" The API answered \"here are the results for a query type you didn't intend\": zero.\n\nI asked \"did my enrichment run?\" The platform answered \"did the container exit without crashing?\": yes.\n\nI asked \"does search work?\" My terminal answered \"does search work when the input is corrupted?\": no.\n\nEvery layer answered truthfully. None of them answered my question.\n\n**What I do differently now**\n\nThree rules, written into my project docs so future-me can't argue with them:\n\nVerify against a known-positive, not a random sample. Testing sanctions lookup with a company that has no sanctions proves nothing, because a broken implementation and a correct one give identical output. You need a case where the right answer is not empty.\n\nExit code 0 is not evidence of work. Check the artifact. Did the file's timestamp change? Did the row count move? Did the summary log print? A scheduler dashboard tells you about the container's lifecycle, not about your program's intent.\n\nRun scheduled jobs manually once, immediately after registering them. The gap between \"registered\" and \"actually working\" is where silent failures live for days. Five minutes now versus a week of green checkmarks later.\n\n**\n\nThe broader version of this**\n\nI've been building an API whose customers are AI agents rather than people, and this problem gets worse in that context, not better. A human user who gets \"no sanctions found\" for a company they know is sanctioned will email you. An agent will write it into a report and move on.\n\nAgents don't have the intuition that catches a wrong-but-plausible answer. They can't tell \"confidently empty\" from \"correctly empty.\" Whatever you return, they'll act on.\n\nWhich means for agent-facing APIs, the distinction between empty and broken isn't a nice-to-have in your error handling. It's the product. I ended up encoding it explicitly: a genuine no-match returns 200 with an empty array and a note saying this is a definitive result, not an error; an upstream failure returns 503 with an error code. Same absence of data, two completely different meanings, and the consumer has to be able to tell them apart without guessing.\n\nThree bugs that said \"success\" taught me that. I'd rather have learned it from someone else's post, which is why I'm writing this one.", "url": "https://wpnews.pro/news/three-bugs-that-reported-success-what-exit-0-doesn-t-tell-you", "canonical_source": "https://dev.to/wonderfulian/three-bugs-that-reported-success-what-exit-0-doesnt-tell-you-1dcf", "published_at": "2026-09-28 03:28:51+00:00", "updated_at": "2026-09-28 03:47:56.992265+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools"], "entities": ["Cloud Run", "Cloud Scheduler", "Git Bash", "Node.js"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/three-bugs-that-reported-success-what-exit-0-doesn-t-tell-you", "markdown": "https://wpnews.pro/news/three-bugs-that-reported-success-what-exit-0-doesn-t-tell-you.md", "text": "https://wpnews.pro/news/three-bugs-that-reported-success-what-exit-0-doesn-t-tell-you.txt", "jsonld": "https://wpnews.pro/news/three-bugs-that-reported-success-what-exit-0-doesn-t-tell-you.jsonld"}}