ConstructiCat Logo
CodeBust.
Browse section ▾

Happy-Path-Only Code.

AI assistants tend to generate code that handles only the successful, well-formed case — skipping input validation, error handling, null/empty checks, and edge cases — so the code works in the demo and breaks in production.

##Signs and Symptoms

A reviewer spots happy-path-only code when every line assumes the previous one succeeded: network calls aren't checked for non-2xx status, JSON.parse/await res.json() is never wrapped, nullable fields are dereferenced directly, arrays are indexed without checking length, and external input flows straight into the logic with no validation. There is usually exactly one return path and no throw, no guard clauses, and no catch (or a catch that's empty or just console.log).

// AI-generated: only the success scenario exists
async function getUserCity(userId) {
  const res = await fetch(`/api/users/${userId}`);
  const user = await res.json();
  return user.address.city.toUpperCase();
}

Failure modes silently absent: res.ok is never checked (404/500 returns an error body that .json() may reject on), user could be {}, address could be null, city could be undefinedCannot read properties of undefined. The function "works" against a happy stub and throws against reality.

Tell-tale signs in a diff:

  • A new async function with no try/catch and no .catch() on the promise.
  • Direct property chains (a.b.c.d) on data that crossed a trust/IO boundary.
  • Unchecked arr[0], find(...)!, as casts, or non-null assertions (!) standing in for real handling.
  • // TODO: handle errors or a bare catch (e) {} left as a placeholder.
  • The PR description says "handles X" but only the X-succeeds branch is implemented.

##Reasons for the Problem

Why models produce it

  • Next-token probability favors the canonical flow. The most probable continuation after const user = await res.json() is return user.something — not a status check. Error handling is high-variance boilerplate that varies per codebase, so it is statistically "surprising" and gets dropped.
  • Training data is happy-path-biased. Tutorials, README snippets, blog posts, and accepted Stack Overflow answers strip out validation and error handling for brevity ("error handling omitted for clarity"). The model learned from text that deliberately removed the very code you want.
  • Sycophancy / reward-for-looking-clean. RLHF-tuned assistants are rewarded for concise, directly-responsive answers. Defensive code looks like noise, so the model optimizes for the tidy snippet that appears to answer the prompt.
  • No repo context. The model doesn't know you have an AppError type, a Result<T> wrapper, a zod schema, or a logging convention, so it can't reuse them — and defaults to "nothing" rather than guessing your conventions.
  • Documented behavioral tendencies. OX Security's analysis of 300+ repos names Avoidance of Refactors and By-the-Book Fixation among its top AI anti-patterns — the model emits functional code for the immediate prompt and never hardens it. arXiv 2509.20491 catalogs AI-specific smells around silent failures; arXiv 2510.03029 finds elevated implementation smells like Empty Catch Block in LLM output.

Why it hurts

  • Correctness: crashes on null, empty collections, timeouts, and non-200 responses — exactly the inputs that don't appear in a quick manual test.
  • Security: the happy path implicitly trusts its input. Skipped validation at boundaries is how injection, path traversal, and prototype-pollution bugs enter. OX's report frames this as code that is "insecure by dumbness," shipped fast without judgment.
  • Review load & debugging: in the 2025 Stack Overflow Developer Survey, 66% of developers cite "AI solutions that are almost right, but not quite" as their top frustration and 45% say debugging AI-generated code is more time-consuming. Happy-path code is the archetype of "almost right" — it reads fine and fails at runtime.
  • Tech-debt accrual: because the model doesn't refactor or reuse existing error utilities, each happy-path function is a fresh single-use blob. GitClear's 2025 data shows the broader pattern — copy/pasted lines rose from 8.3% (2021) to 12.3% (2024), 5+-line duplicate blocks grew ~8x in 2024, while refactoring (moved lines) fell from ~25% to under 10%. Missing error handling gets bolted on later, duplicated per call site, never centralized.

##Treatment

Review & prompting tactics

  • Make the model enumerate failure modes first. Prompt: "Before writing code, list the failure modes for this function (bad input, network error, non-200, empty/malformed response, missing fields, concurrency). Then implement handling for each." Forcing the enumeration step counteracts the next-token tendency to skip them.
  • Point at the conventions to reuse. "Use our existing AppError/Result type from lib/errors.ts and the logger from lib/log.ts; validate the response with the zod schema in schemas/user.ts." This converts "no handling" into reuse instead of a bespoke blob (avoids Duplicate Code).
  • Require the gates to run. "Run eslint and tsc --noEmit and fix every warning, including @typescript-eslint/no-floating-promises." Type-checking with strictNullChecks turns silent null derefs into compile errors the model must address.
  • Demand the negative tests. Ask for unit tests covering the empty/null/error inputs, not just the happy case — the absence of those tests is itself the smell.

The refactor

Add boundary validation and guard clauses, and centralize handling. Named moves: Introduce Guard Clause (early throw on invalid state), Introduce Assertion / boundary validation (parse-don't-validate at IO edges), and Introduce Special Case / Null Object (?? "UNKNOWN" instead of crashing).

// after: failure modes are first-class
async function getUserCity(userId: string): Promise<string> {
  if (!userId) throw new InvalidArgumentError("userId required");   // guard clause

  const res = await fetch(`/api/users/${encodeURIComponent(userId)}`);
  if (!res.ok) throw new ApiError(`user fetch failed: ${res.status}`);

  const user = UserSchema.parse(await res.json());                  // validate at the boundary
  return user.address?.city?.toUpperCase() ?? "UNKNOWN";           // special-case the gap
}

If the same try/validate/log pattern starts repeating across call sites, Extract Function it into a shared fetchJson<T>(url, schema) helper so error handling lives in one place rather than being copy-pasted (the GitClear duplication trap).

##Detected by

  • typescript-eslint @typescript-eslint/no-floating-promisesFlags promises whose rejection path is never handled (no await/catch) — a common happy-path symptom for async calls.
  • ESLint no-emptyFlags empty blocks including empty catch blocks — the placeholder 'error handling' the model leaves behind.
  • SonarSource (JS/TS) javascript:S2486Exceptions should not be ignored — flags caught-but-swallowed errors, the degenerate cousin of no handling at all.