Every few months a new thread claims one model has definitively "won" at coding, usually based on a leaderboard score that has nothing to do with the kind of work most of us actually do day to day: reading someone else's messy function, figuring out why a request is throwing a 500, or writing the fortieth CRUD form validator this year. So instead of trusting a benchmark, I picked four tasks I hit almost every week, ran each one through ChatGPT, Claude, and Gemini over about three weeks of real work, and paid attention to where each tool actually helped versus where it politely wasted my time.
This isn't a scored shootout with a winner crowned at the end. It's notes on where each model's habits show up in practice, so you can pick the right one for the task in front of you instead of whichever one has the loudest fans on social media this month.
Task 1: Debugging a stack trace
The test case: a Flask route throwing a KeyError three layers deep inside a nested dictionary built from a joined database query, with the actual bug being a silent schema mismatch rather than anything obvious in the traceback itself.
- ChatGPT jumped straight to a fix — it pattern-matched on "KeyError in nested dict" and suggested defensive
.get()calls almost immediately. That's useful when the fix really is that simple, but here it papered over the real problem (a column that had been renamed in a migration) rather than finding it. I had to explicitly ask "why would this key be missing in the first place" before it dug into the actual cause. - Claude asked to see the surrounding query and model definitions before proposing anything, and it was the one that flagged the migration mismatch on its own without me pointing at it. Slower to a first answer, but the first answer was more often the right one.
- Gemini was strongest when I gave it more surrounding file context at once (it handles large pasted blocks well), but in a plain chat window with just the traceback and the one function, it gave a generic "here are five common causes of KeyError" answer that read like a Stack Overflow summary rather than a diagnosis of my specific bug.
If your stack trace is long, it's worth trimming it to the frames that actually matter before you paste it in — dumping forty lines of framework internals just burns context and buries the part the model needs to reason about. Running the trace through something like a word counter before pasting is a quick sanity check on whether you're about to hand over more noise than signal.
Task 2: Refactoring a function for readability
I gave each model the same 60-line JavaScript function: three levels of nested callbacks handling a file upload, with error handling scattered unevenly and two variables reused for unrelated purposes.
ChatGPT produced the most immediately "clean-looking" result — consistent formatting, promises instead of callbacks, sensible variable names. But it also quietly changed the error-handling behavior in one branch (swallowing an error that the original code re-threw), and it didn't mention that it had done so. That's the kind of change that passes a quick read-through and then bites you three weeks later.
Claude's refactor was less aggressive — it kept closer to the original control flow and called out, in prose, the specific behavior it preserved versus the one thing it thought was actually a bug worth fixing separately. That extra narration takes longer to read, but it's the difference between a refactor you can trust and one you have to re-verify line by line.
Gemini split the difference: solid mechanical refactor, but when I asked it to explain what changed and why, the explanation was thinner than Claude's and didn't catch the same error-handling nuance ChatGPT had introduced (because Gemini's own refactor didn't introduce it, but it also didn't proactively flag that it was a common gotcha in this pattern).
Task 3: Explaining unfamiliar code
Second-hand codebases are where "explain this" prompts either save you an afternoon or send you down the wrong path with false confidence. My test here was a legacy PHP abstract factory setup — the kind where the actual class that gets instantiated depends on three levels of config lookups that aren't visible in the file you're looking at.
All three models correctly identified the pattern as a factory. Where they diverged was honesty about what they couldn't see. Claude explicitly said something close to "I can explain the factory pattern here, but I can't tell you which concrete class actually gets returned without seeing the config values — can you paste those?" ChatGPT gave a plausible-sounding guess at which class would be instantiated based on naming conventions, and it happened to be wrong for this codebase. Gemini landed in between — it explained the mechanism accurately but didn't flag the specific unknown (the config-driven branch) as something to verify.
The lesson from this one wasn't really about which model is "smarter" — it's that when a model has to guess at something outside the pasted code, some of them tell you they're guessing and some don't. That distinction matters more than raw explanation quality, and it's worth testing deliberately, the way I break down in more detail in where AI tools quietly get things wrong.
Task 4: Generating boilerplate
For repetitive scaffolding — a Laravel form request class, a PHPUnit test skeleton, a basic CRUD controller — the differences narrowed considerably. This is the category where all three models are genuinely good, because the output is well-represented in training data and there isn't much ambiguity to reason through.
A representative prompt and the kind of output that actually held up across all three:
Generate a Laravel FormRequest class named UpdateInvoiceRequest.
Fields: amount (decimal, required, min 0.01), due_date (date, must be
today or later), status (string, must be one of: draft, sent, paid,
void). Include custom error messages for the amount and status rules.
All three produced working, idiomatic code on the first try. The differences showed up only at the edges: ChatGPT was fastest to also suggest a matching factory/seeder if I mentioned testing; Claude was more likely to ask a clarifying question if a rule was genuinely ambiguous (for instance, whether due_date should allow today or only future dates) instead of silently picking one interpretation; Gemini's output was fine but occasionally used array-based validation syntax from an older Laravel convention that needed a small tweak.
Side-by-side summary
| Task | ChatGPT | Claude | Gemini |
|---|---|---|---|
| Debugging a stack trace | Fast, sometimes fixes the symptom not the cause | Slower, more likely to find root cause | Needs more context to avoid generic answers |
| Refactoring for readability | Cleanest output, can silently change behavior | Conservative, explains what it preserved vs. changed | Solid mechanical refactor, thinner explanations |
| Explaining unfamiliar code | Confident, occasionally guesses without flagging it | Explicit about what it can't verify from the code shown | Accurate on mechanism, less proactive about unknowns |
| Generating boilerplate | Fast, adds helpful extras unprompted | Asks clarifying questions on ambiguous rules | Good, occasional outdated syntax convention |
What the table doesn't capture
A comparison table flattens things that matter in daily use. Two habits stood out enough over three weeks that I now factor them into which tool I open for a given task:
- How each model handles being told it's wrong. When I pushed back with "that's not actually the bug," ChatGPT tended to apologize and pivot to a new guess quickly — sometimes too quickly, without re-examining the original reasoning. Claude was more likely to ask what specifically was wrong about its answer before revising, which produced better second attempts.
- Context window habits in longer sessions. In a single chat thread that had accumulated a lot of back-and-forth, Gemini and Claude both stayed more consistent about earlier constraints I'd mentioned (like "we're on PHP 8.1, no readonly properties"). ChatGPT occasionally reverted to defaults from earlier training after a long thread, suggesting an older PHP version's syntax even after I'd corrected it once.
One prompt, three different follow-up questions
A smaller thing that ended up mattering more than I expected: what each model asks you next, when it asks anything at all. I ran the exact same underspecified request through all three — "write a function to merge two sorted arrays of objects by a date field, and handle duplicates" — with no other context given, deliberately leaving out details a real task would need.
ChatGPT wrote a complete function immediately, picking its own defaults for the ambiguous parts (it assumed "duplicate" meant identical date values and kept the first occurrence, without saying so unless I asked). That's a fine default a lot of the time, and if you're moving fast it's genuinely convenient — you get something runnable in one message. But the assumption is buried in the code, not stated, so if your actual definition of "duplicate" was different (say, matching on an ID field rather than date), you'd only find out by reading the implementation closely enough to reverse-engineer what it decided.
Claude wrote a version too, but opened with two direct questions before the code: what field defines a duplicate, and what should happen to the discarded one — dropped, or merged into the kept record. It answered its own questions with a reasonable default and labeled that default clearly in a comment, which meant I didn't have to choose between waiting for clarification and getting unlabeled guesswork — I got both at once.
Gemini gave a working function with brief inline comments explaining the duplicate-handling choice, similar to Claude's approach but with less explicit flagging of it as an assumption versus just a design decision. Functionally solid, but you have to read the comments carefully to notice that a decision was made on your behalf rather than assume the code is unambiguously "the" correct implementation.
None of these responses were wrong, and none of them are a permanent trait of a specific model — they're habits that show up often enough to plan around. If you're going to hand off a task with real ambiguity in it, it's worth explicitly asking the model to state what it assumed rather than relying on any one tool's default behavior to surface it for you.
Which one to actually reach for
My working rule after this: Claude first when I need to trust the answer without re-verifying every line — root-cause debugging, refactors on code I don't want to silently break, anything where "I don't know" is a better answer than a confident guess. ChatGPT first for speed on well-defined, low-ambiguity tasks where I can eyeball the output quickly — boilerplate, quick syntax questions, first-draft scaffolding. Gemini earns a look when I'm working inside a large amount of pasted context at once, since it tends to hold up better there than in short, context-free chat exchanges.
None of this is fixed — these tools update on their own schedule and today's habits might shift in a few months. The more durable point is the method: test the two or three tasks you actually do, not a synthetic benchmark, and keep the differences somewhere you can refer back to. If you want a more systematic way to get better output out of whichever model you land on, the patterns in five prompt engineering patterns that actually improve output quality apply to all three of these tools equally.
No comments yet.
Be the first visitor to add a thoughtful comment on this article.