Assessments of coding assistants tend to arrive as either transformation or dismissal, and neither survives sustained use. The tools are useful, the usefulness is uneven, and the pattern in where they succeed and fail is consistent enough to be worth stating directly.
The organising question is not whether the output is correct. It is whether checking the output costs less than producing it would have.
Where they clearly help
The strongest case is code that is tedious rather than difficult. Structural boilerplate, data transformations between known shapes, configuration in a format you use rarely, and test scaffolding around behaviour you have already decided. These are tasks where you know exactly what you want and the work is transcription.
The second strong case is unfamiliar surface area. Working against an API you have not used before, in a language you use occasionally, or with a library whose conventions you do not know. The assistant compresses the time between intent and a working first attempt, which is the slowest part of that work.
The third is explanation. Reading unfamiliar code and asking what a section does, or why an approach was chosen, is a genuinely good use, because you retain the ability to verify the answer against the code in front of you.
Where they reliably fail
Reasoning that spans many files degrades quickly. The assistant sees what it retrieves, and if the relevant constraint lives in a file that was not retrieved, the output will be locally sensible and globally wrong. This is the most common source of confidently incorrect changes in a large codebase.
Anything where plausible and correct diverge is dangerous. Concurrency, resource lifetimes, error handling on unusual paths, and security sensitive code all share the property that wrong code looks right. The assistant is optimising for plausibility, and in those areas plausibility is exactly the wrong target.
Novel logic is weaker than it appears. Code that resembles a well established pattern is produced well, while code implementing a genuinely unusual requirement tends to be produced as the nearest familiar pattern with the requirement bolted on.
The review problem
This is the part that is consistently underestimated. Reading code carefully is harder than writing it, and generated code arrives in a form that defeats the reader's usual defences.
Human written code carries signals. Hesitation shows up as awkward naming, inconsistent structure, or a comment admitting uncertainty, and reviewers use those signals without noticing. Generated code is uniformly fluent, so a subtly wrong implementation reads exactly like a correct one, and the reviewer's instinct for where to look harder receives nothing to work with.
The consequence is that time saved in writing can reappear, larger, in review. A change that took two minutes to generate and thirty to verify properly was not a saving, and the temptation is to skip the thirty.
Two failures worth naming
The first is invented dependencies. A model asked for code using a library it half remembers will produce an import for a package that does not exist, or a function that was removed several versions ago. Most of the time this fails loudly at install or at runtime, which is harmless. The dangerous case is that plausible package names are predictable, so a name a model invents repeatedly is a name an attacker can register and publish to. A dependency that appeared because a tool suggested it deserves the same scrutiny as one a stranger suggested.
The second is drift in consistency. Each suggestion is locally reasonable and shaped by the general distribution of code rather than by the conventions of your repository. Accepted steadily without correction, the result is a codebase where similar problems are solved several different ways, none wrong and none matching. That cost is invisible per change and substantial over a year, and it is paid by whoever maintains the code afterwards.
The effect depends on experience
An experienced engineer gains most, because they can evaluate output quickly and reject bad suggestions almost without conscious effort. For them the tool removes typing rather than thinking.
Someone still building judgement is in a harder position, because the same fluency that defeats reviewers also defeats learning. Accepting a working solution without understanding why it works removes the struggle that builds the ability to evaluate the next one. The tool is most confident precisely where the user is least equipped to check it.
This is not an argument against use. It is an argument for using it deliberately, asking for explanations rather than only solutions, and treating output as a proposal to be understood rather than an answer to be accepted.
What works in practice
Keep the scope small. A well defined function with clear inputs and outputs produces good results, while a vague instruction to implement a feature produces something that requires more correction than it saved.
Give the constraint explicitly. The assistant does not know your conventions, your performance requirements, or the invariant that made the existing code look strange, and stating those in the request is faster than correcting their absence afterwards.
Let the tests do the checking
Let tests be the contract. Generated code that must pass tests you wrote is verified by something other than reading, which is the single most effective way to reduce the review burden.
And be honest about the accounting. If verifying a change takes longer than writing it, the tool did not help on that task, and recognising that quickly is more valuable than persisting with it.
Note: the tool is strongest where you can check the answer fastest, and most dangerous where checking is slow. That relationship, rather than the difficulty of the task, is the reliable predictor of whether it will help.