Symptom Fixes
An error is a symptom, and its cause is the condition that produced it. An agent asked to make an error go away will often do exactly that: catch it, retry past it or default around it, and leave the cause in place, now silent. The crash was evidence, and a symptom fix removes the evidence.
Symptom fixes are the most tempting shape in this chapter, because they look like diligence. The diff handles an error that was not handled before, the suite is green and the crash report stops. What changed is that the system now does something wrong without telling anyone.
Three Shapes
The first shape is catch and continue: the failure is caught, logged and replaced by a default value, and execution carries on as though the default were the answer. The second is retry until it passes: the failing operation is repeated after a short delay until a flaky outcome happens to come out right. The third is step around it: a new check skips the case that crashed, without anyone asking why that case occurs.
The Refund Case
After the redirect at the end of Chapter 6, the agent restored the gym's time zone in the deadline calculation, through the helper in schedule. That surfaced a problem the first attempt had hidden. One of Cragmoor's four gyms has no time zone on record: it was added before the field existed, and nobody filled it in. For that gym the zone lookup fails.
Asked to handle that gym, the agent reached for a catch. When the lookup fails, it logs a warning and falls back to the server's time zone. The first topic of this chapter found an invented method inside this branch; with that corrected, the code below runs, the crash is gone and every test passes.
def refund_deadline(starts_at: datetime, gym: Gym) -> datetime:
"""The last moment a cancellation still gets a full refund."""
# Class times are stored on the gym's wall clock, with no zone attached.
try:
start = schedule.localize(starts_at, gym.time_zone)
except (TypeError, ValueError):
# Gym has no time zone on record: fall back to server time.
log.warning("gym %s has no time zone; using server time for the %d-hour window",
gym.code, FULL_REFUND_WINDOW // timedelta(hours=1))
start = starts_at.replace(tzinfo=schedule.SERVER_ZONE)
return start - FULL_REFUND_WINDOW
gym south has no time zone; using server time for the 12-hour window 0
export function refundDeadline(startsAt: string, gym: Gym): Date {
// Class times are stored on the gym's wall clock, with no zone attached.
try {
const start = schedule.localize(startsAt, gym.timeZone);
return new Date(start.getTime() - FULL_REFUND_WINDOW_MS);
} catch {
// Gym has no time zone on record: fall back to server time.
console.warn(`gym ${gym.code} has no time zone; using server time`);
const start = schedule.localize(startsAt, schedule.SERVER_ZONE);
return new Date(start.getTime() - FULL_REFUND_WINDOW_MS);
}
}
gym south has no time zone; using server time 0
public static ZonedDateTime refundDeadline(LocalDateTime startsAt, Gym gym) {
// Class times are stored on the gym's wall clock, with no zone attached.
ZonedDateTime start;
try {
start = Schedule.localize(startsAt, gym.timeZone());
} catch (NullPointerException | DateTimeException e) {
// Gym has no time zone on record: fall back to server time.
LOG.warning("gym " + gym.code() + " has no time zone; using server time for the "
+ FULL_REFUND_WINDOW.toHours() + "-hour window");
start = startsAt.atZone(Schedule.SERVER_ZONE);
}
return start.minus(FULL_REFUND_WINDOW);
}
Oct 29, 2026 5:01:38 PM org.cragmoor.Refunds refundDeadline WARNING: gym south has no time zone; using server time for the 12-hour window 0
That gym is in the second time zone, an hour behind the servers. Its deadline is now computed an hour early, so a member who cancels at 20:30 on Friday, inside the window the rule promises, is refused $24.00. Nothing fails. The only trace is a warning in a log nobody reads, and the first person to notice will be a member who knows the rule better than the code does.
The Retry That Hides the Race
Suppose a waitlist test that runs a cancellation and the sweep side by side fails about three runs in a hundred, the rate Chapter 10 measures. Asked to fix the flaky test, an agent wraps the step that sends an offer in a retry with a short delay. The next fifty runs are green, and the flakiness is declared fixed.
The flakiness was the evidence. The test failed intermittently because two paths, a cancellation and a background sweep, can both read the same free place before either writes an offer, and that race is how one place is offered to two members, the bug Chapter 10 reproduces and fixes at its root. The retry changed nothing about the race. It changed only how often the test catches it, and so it removed the one signal pointing at the bug.
Why It Happens
The agent sees the error text and the instruction to make things pass. The shortest change that removes the text sits exactly where the text came from: the line that raised it, the call that failed, the assertion that went red. The cause is often somewhere else, in another file, in the data or in the timing, and none of those appear in the error message.
Nothing in the agent's loop asks it to keep going once the run is green. "Make the error go away" was the task, and the error went away. The agent fixed what it was shown, and what it was shown was the symptom.
Asking for the Cause
Before accepting any fix for an error, ask why the condition occurs, and accept only an answer that names a mechanism. "The lookup can fail" is not a cause. "This gym's record has no time zone because the field was added after the gym was" is one: you can point at the record and the migration that skipped it. "The test is flaky" is not a cause. "Two paths read one free place before either writes an offer" is one.
Then decide where the fix belongs. The missing zone belongs in the data: fill in the field for that gym and make it required, so the next gym cannot be added without one. The code can then fail loudly if a zone is ever missing, because that would be a new defect, not a known one. The race belongs in the code that claims a place. Chapter 10 is the full method.
In practice the question is one message, sent before the fix is accepted. Below, the agent's first answer describes the symptom again in other words; the follow-up asks for something it can only answer by looking at the data, and the second answer names a record and a date. That second answer tells you where the fix belongs, and it is not in the refund code.
Spotting Them in Review
Symptom fixes have recognisable shapes in a diff: a new catch block, a new retry, a new check that skips a case, or a default added beside a line that used to fail. Each one needs the same question before it is accepted: what made this fail, and is that still true? If the answer is "yes, but now we cope", the diff has hidden a defect rather than fixed one.
The question also has a cheap mechanical form. Search the diff for the words that symptom fixes are built from: a new catch block, a retry, a sleep, a "skip if", a default value, a log line at warning level. None of them is wrong in itself. Each one in an agent's diff for an error is a place where the agent may have decided, on your behalf, that a failure is acceptable, and that is a decision you want to make yourself.
- "Catching the exception handles the error." It stops the crash. Handling means deciding what is correct for that case, and "use server time" is a decision made silently, by the agent, for a gym it knew nothing about.
- "Retries fix flaky tests." A flaky test is either a flaky test or an intermittent bug, and a retry turns both green and hides which one it was.
- "The fix that stops the error is the fix." The error was the evidence. Removing it leaves the cause in place and destroys the trail that led to it.
- "The agent fixes symptoms because it does not understand the code." It fixed what it was shown. The error message points at the symptom, so a request built on the message gets a symptom fix.
- "Continuing is always better than crashing." For a refund deadline, a crash at a gym with no zone is seen and fixed in an hour, and a wrong deadline is found by a member weeks later.
- Ask why the condition occurs before accepting any fix for an error, and accept only an answer that names a mechanism. A cause is something you can point to.
- Treat every new catch, retry or skip in a diff as a decision that needs a stated reason. These are the forms symptom fixes take.
- Fix bad data in the data, and make the field required. A default in code around missing data keeps the data missing forever.
- Never retry a flaky test into green before finding out why it is flaky. The flake may be the bug.
Knowledge Check
One gym has no time zone on record. What should the refund deadline code do for it?
- Fall back to the server's own time zone and log a warning naming that gym
- Fail loudly, while the zone is filled in and the field made required
- Skip the refund check for that gym until someone adds its missing time zone
- Guess the zone from the nearest other gym and carry on with the deadline
A waitlist test fails about three runs in a hundred. An agent adds a retry with a short delay, and the suite is green. What happened to the evidence?
- The retry fixed the race, so there is no longer any evidence to keep
- The retry logs every failed attempt, so the evidence is now in the logs
- The race is unchanged, and the one signal pointing at it is now hidden
- The test was flaky rather than the code, so there was no evidence lost
An agent proposes a catch block for an error. Which follow-up question turns the review towards the root cause?
- Do all of the existing tests still pass with the new catch in place?
- Could the catch block log the error at a higher severity level?
- Can the catch block be narrowed to the one exception type it needs?
- Why does this condition occur, and what mechanism produces it here?
Which of these additions in a diff is most likely to be a symptom fix that needs a stated reason?
- A rename of a variable inside a function the task asked to change
- A default value returned right beside the line that used to fail
- A new test that pins the boundary of the refund rule at 12 hours
- An updated cancellation email that now says 12 hours, not 24
You got correct