📝 Originally published (in Japanese) at forge.workstyle.tech.
When you encounter a bug, the quickest fix is to "eliminate the symptoms." If an error occurs, wrap it in a try-catch and swallow it. If it breaks only with a specific value, avoid that value via hardcoding. If the precision is off, boost it with a heuristic keyword to fake the result. All of these seem to work temporarily.
However, these quick fixes will inevitably come back to bite you. Because the root cause remains alive, the same problem will resurface through a different entry point. Swallowed errors leak downstream in much more cryptic forms. Hardcoded conditions become landmines for the next developer making a change.
When working with AI coding agents (like Claude Code), this temptation actually intensifies. Agents can suggest "fixes that work for now" at high speed. This is precisely why it is effective to explicitly impose a principle on the agent: "Ban quick fixes; always strive for the root cause resolution." In this article, I will introduce a pattern for investigation—reaching the root cause without hiding the symptoms—using three bugs I actually encountered while developing a voice conversion app.
The Grand Principle: Eliminate the Root Cause, Not the Symptom
First, let me establish the decision-making criteria that run through this article. When a proposed fix is presented, ask yourself the following:
- Are you eliminating the symptom or the root cause?
- Will this fix also eliminate other symptoms derived from the same root cause?
- Can you explain why the fix works in a single sentence?
The third point is particularly crucial. A fix that you cannot explain is usually just hiding a symptom. Saying "If we avoid this value, it won't crash" is not an explanation. Saying "It crashes because the assumption of [X] breaks when this value is provided; therefore, I made it so the assumption is always met" is an explanation.
Let's look at three real-world examples.
Case Study 1: Converted Audio "Speaks Slowly" — Proportionality Points to the Root Cause
The first symptom was that only the voice-converted audio would play back with an unnaturally stretched cadence. The input recording was at a normal speed, but the output sounded like a slow, drunken speech.
One could think of endless quick fixes. For example, applying time-stretching to the output to force it back to normal speed. However, that explains nothing about why it was slow in the first place.
What worked here was the observation of proportionality. The issue didn't occur with short audio files, only with long recordings. Moreover, the longer the input, the slower the output became. A 131-second recording resulted in playback over 4 times slower than normal—this clue, that the "issue worsens in proportion to length," pointed me directly to the location of the root cause.
If it were a sampling rate mismatch, the audio would be consistently slow by a fixed ratio, regardless of length. The same applies to a time-stretch bug. A proportional relationship where "the issue scales with length" only exists when a fixed-length segment is being stretched to fit the total duration.
The root cause was the "30-second limit" of the Whisper encoder used for feature extraction. When I passed a 131-second recording, it only retrieved the content for the first 30 seconds. Since that 30-second chunk was being stretched to 131 seconds, it became 131 ÷ 30 ≒ 4.4x slower. This matched my "over 4x slower" perception perfectly.
The solution was to split the audio into overlapping 30-second chunks, run each through Whisper, and concatenate the results. This wasn't a symptomatic time-stretch; it was a fix at the source—ensuring correct information is obtained during the feature extraction stage. I documented the technical details of this investigation in a separate article: "The culprit behind the 'low speech' bug in voice conversion was Whisper's 30-second limit."
The lesson here is simple: Proportionality is an arrow to the root cause. If you measure what the symptom scales with, you can mechanically narrow down the suspects.
Case Study 2: 502 Errors During Long ML Inference — Don't "Extend" Timeouts, "Change the Mechanism"
Next was a bug where attempting to generate long audio with a 44.1kHz wideband model resulted in a 502 error at the frontend. Short audio worked fine, but if generation took too long, it inevitably resulted in a 502.
The easiest quick fix is to set the timeout value to a massive number. However, this is a classic symptomatic treatment: tinkering with numbers without understanding why the connection is dropping. Even if you increase the number, it will just crash again once an input exceeds that new limit. You've just postponed the landmine.
By chasing the root cause, I discovered that when the Next.js server relayed requests to the inference backend, the internal fetch implementation (undici) had a default timeout. It was closing the connection because it couldn't wait for the long-running response. The 502 was the result of the proxy layer giving up while the upstream server was still alive.
This is where the decision path diverges. "Disabling the undici timeout" would technically work, but the more robust solution was to replace the proxy relay with Node's standard http/https and allow unlimited waiting for a response. Given the nature of long-running inference, the very premise of "cutting off after a certain time" was incompatible with this endpoint. Therefore, the fix was to change the implementation so that this assumption was removed—treating the root cause.
The lesson here is: When you feel the urge to tinker with "numbers" like timeouts or retry counts, stop and ask if this is just symptomatic treatment. In many cases, you shouldn't be adjusting the number; you should be questioning "why is the architecture designed such that this limit exists?"
Case Study 3: Massive Model Download Stalls — Don't "Ignore and Proceed," "Ensure Placement"
The third issue involved the process of fetching multi-gigabyte model weights from HuggingFace stalling halfway through. In environments with unstable networks, the download would simply stop silently and hang.
The temptation for a quick fix here was to "swallow the download failure and attempt to continue starting the app." However, if you attempt inference with incomplete model weights, you'll just encounter much more confusing errors later in the pipeline. Swallowing the error merely hides the problem; it doesn't solve it.
The root cause was that the standard downloader could not detect "stalling" (silently stopping); once it got stuck, it couldn't recover on its own. It didn't crash, and it didn't return an error; it just sat there silently. This meant there was nothing to "swallow"—no exception was being thrown in the first place.
The solution was to use a downloader that supports stall detection and resumption (using specific curl options) and ensure the artifacts are reliably placed in the HuggingFace cache directory. If no data flows for a certain period, the process treats it as a failure, interrupts, and resumes from where it left off. This guarantees that a complete file eventually lands in the cache, even on unstable networks.
The lesson here is: "Silent stalling" is more troublesome than "crashing with an error." You cannot swallow an exception that is never thrown. The correct approach is to provide a reliable acquisition mechanism and define "completion" as the moment the artifact is successfully and accurately placed.
The Common Pattern Found in These 3 Cases
While these are three different bugs, the pattern used to reach the root cause is the same:
- Measure the "Effectiveness" of the Symptom — As in Case 1 ("proportional to length"), observe what the symptom scales with. Proportionality, boundaries, and reproduction conditions are direct arrows to the root cause.
- Eliminate the "Likely Suspects" First — Like testing sampling rates or time-stretching, eliminate suspicious candidates based on observed facts. What remains points to the root cause.
- Use "Can you explain why it works in one sentence?" as a Gatekeeper — If a fix cannot be explained, suspect it is merely hiding a symptom. In Case 2, "increasing the timeout" was not an explanation, so it failed the test.
- Be Wary of Tinkering with Numbers and Swallowing Errors — Increasing timeout values (Case 2) or swallowing errors (Case 3) are classic signals of symptomatic treatment. If you reach for them, stop and think.
Practicality When Working with AI Agents
This principle is worth institutionalizing specifically when working with AI coding agents. Because agents can rapidly mass-produce quick fixes, if left unchecked, you will end up with a mountain of code that "works, but only hides the symptoms."
What is effective is to define a permanent instruction for the agent: "Prohibit quick fixes. Follow this sequence: Identify root cause $\rightarrow$ Appropriate technology selection $\rightarrow$ Propose design-level solution $\rightarrow$ Implementation." Then, when a proposal is made, the human must put it through the gate: "Is this the symptom or the root cause?" and "Can you explain why it works in one sentence?" Agent productivity and the discipline of root-cause resolution can coexist.
Summary
- Quick fixes (swallowing errors with
try-catch/ escaping via hardcoding / faking with heuristics) preserve the root cause and will inevitably recur. - Evaluate fix proposals using two gates: "Is this eliminating the symptom or the root cause?" and "Can you explain why it works in one sentence?"
- Case 1: The proportionality of the symptom pointed to the root cause (Whisper's 30-second limit). Proportionality is an arrow to the root cause.
- Case 2: The 502 during long inference was solved not by increasing timeouts, but by changing to a relay mechanism that doesn't cut off by time.
- Case 3: The stall in massive downloads was solved not by swallowing the error, but by using stall detection + resumption to ensure the artifact is placed reliably.
- AI agents are prone to mass-producing quick fixes. Make "strive for root-cause resolution" a permanent instruction, and use humans to act as the gatekeepers.
Top comments (0)