TL;DR: A prompt for working code needs four things a vague prompt skips: the exact language version, the dependencies already in use with their versions, how you want errors handled, and whether you want tests included. State those four explicitly and most code that doesn't compile on the first try starts compiling instead.
Why Does AI-Generated Code Fail to Compile in the First Place?
It is tempting to read a broken build as a reasoning failure: the model didn't understand your codebase. Most of the time that's not what happened. The model answered the prompt you gave it, and the prompt left out information it needed, so it filled the gap with the most statistically common answer from its training data. That answer is frequently a different language version, a different major version of a library, or a different error-handling convention than the one your project actually uses.
This is not a hedge to make the tool look better than it is. It's a specific, fixable, mechanical problem. A prompt for working code fails for the same reason a spec handed to a new contractor fails: not because the contractor can't build, but because "build a deck" without dimensions, materials, or a permit reference gets you a deck, not your deck. Four gaps account for nearly every "looks right, doesn't build" result, and each one has a cheap fix.
What Should You Specify About Language and Version?
This is the highest-leverage fix on the list, and the one most people skip because it feels obvious. "Write this in Python" is not a version. Python 3.9 and Python 3.13 disagree on match statements, on typing syntax, and on several standard-library defaults. TypeScript is an even sharper case: recent versions of the compiler have shifted what counts as strict-mode-correct by default, so code written against an assumed loose config can throw type errors the author never saw, or vice versa, code written assuming strict defaults can compile against a looser config and skip checks you thought you had. "Latest" is not a version either. Training data has a cutoff, and even after that cutoff, "latest" means whatever was most common in what the model saw, which is not the same as whatever shipped last week.
Vague: "Write a function in TypeScript that validates an email."
Specific: "Write a function in TypeScript 5.7, targeting ES2022, with
strict mode enabled, that validates an email address. No external
regex libraries — use a single regular expression."
The second version removes three separate sources of guesswork: which compiler generation, which target runtime, and which strictness setting. None of that changes what the function does. All of it changes whether the function you get compiles against the toolchain you actually have.
How Do You Handle Dependencies in Your Prompt?
The second gap is dependency versions, and the fix is almost embarrassingly simple: paste the relevant lines from your lockfile or manifest instead of naming the library. A fast-moving library can ship breaking changes across major versions inside a single year, and a model has no way to know which side of that break your installed version sits on unless you tell it. This is the same failure mode maintainers warn about with container images: a tag like a language runtime's "latest" is not a fixed target, it moves as new versions ship, which is exactly why teams that care about reproducible builds pin to an exact version or a locked digest rather than trusting an unpinned tag to mean the same thing tomorrow that it means today. The same discipline applies to a coding prompt.
Vague: "Add a function that fetches JSON from an API using axios."
Specific: "Add a function that fetches JSON from an API using axios
1.7.9 (from our package.json). Use the built-in AxiosError type for
error handling, not a generic catch. Target Node 20."
If you don't know the exact version off the top of your head, that's fine — that's what the lockfile is for. Thirty seconds spent grepping package.json, requirements.txt, go.mod, or Cargo.toml and pasting the relevant line into your prompt buys back the ten minutes you'd otherwise spend figuring out why the method the model used was renamed two major versions ago. If you're regularly reaching for the same project context, saving it as a reusable variable instead of retyping it into every prompt is worth setting up once.
How Should You Ask for Error Handling?
"Handle errors properly" is not an instruction, it's a Rorschach test. One reasonable reading throws immediately and lets the caller deal with it. Another catches everything and logs it. Another returns a result or option type instead of throwing at all. Another retries with backoff before giving up. All four are defensible engineering choices, and a model asked to "handle errors properly" without further guidance will pick one — a different one, potentially, on every request, since the choice isn't fully determined by anything else in the prompt. That's how you end up with two functions in the same file that fail in two incompatible ways.
| What you say | What you probably get |
|---|---|
| "Handle errors properly" | An arbitrary pick between throw, log-and-continue, or a result type — inconsistent across requests |
"Throw a custom ValidationError with the failing field name" | A specific, typed exception you can catch by type |
"Return a Result<T, E> instead of throwing" | A value-based error path, no exceptions in the control flow |
"Log to console.error and return null" | Silent-ish degradation — fine for a script, risky for a library |
| "Retry twice with exponential backoff, then throw" | Resilience logic baked into the function itself |
None of these is universally correct. The point is that naming one turns a guess into a decision, and a decision is what compiles consistently the second, third, and tenth time you run a similar prompt. This is the same principle behind writing follow-up prompts that actually improve the answer — a vague follow-up gets a vague revision, and a specific one gets a specific fix.
Should You Ask for Tests in the Same Prompt?
For anything beyond a throwaway script, yes. Asking for even two or three tests alongside the implementation does two things at once. First, it forces the reasoning to happen before the code is finalized — a model asked to also write "the empty-string case" and "the case where the API times out" has to actually consider those cases while writing the function, not just while writing the tests. Second, a test that fails to compile against the function you were just handed is the fastest, cheapest signal you'll get that something in the pairing doesn't line up — faster than running the code yourself and waiting for a stack trace.
"Write a function that parses a CSV row into a User object, plus
3 unit tests covering: a well-formed row, a row with a missing
required field, and a row with an extra trailing comma. Use the
testing framework already in this project (Vitest)."
Naming the testing framework matters as much as naming the language version — a test written for Jest's mocking API dropped into a Vitest project will not run, even though the two look nearly identical on the page.
What Does a Complete Prompt for Working Code Look Like?
Put the four fixes together and you get a template, not a magic phrase — the value is in the specificity, not the wording:
Write [what the code does], in [language + exact version], targeting
[runtime/environment + version]. Dependencies in use: [library@version,
library@version]. Error handling: [your chosen strategy, stated
explicitly]. Include [N] tests using [your testing framework], covering
[the specific edge cases you care about]. Match the style already used
in [brief description or paste a short snippet from your codebase].
Every bracket is a place a vague prompt would have left a decision to chance. You don't need all six blanks filled for a five-line script. You need most of them filled for anything that has to compile against a real project, ship, and still work next month. If you're routinely writing this kind of prompt from scratch, a saved prompt library that stores the template with your project's version and dependency details already filled in turns a two-minute setup into a paste.
What Do You Do When the Code Still Doesn't Compile?
Even a well-specified prompt occasionally comes back wrong, and the fix is not to start over from scratch. Paste the exact error message back, verbatim, not a paraphrase of what you think it means — compiler and interpreter errors are precise on purpose, and "it says something about types" throws away the one piece of information that would let the model fix the actual problem instead of guessing at a new one. This is the same discipline behind red-teaming your own prompt before you trust the output: treat the first response as a draft to interrogate, not a final answer to accept or discard wholesale. If you've hit a wall where the model keeps declining to touch a section of code entirely, that's a different problem with its own fix.
What Do Common Failure Signatures Actually Tell You About the Prompt?
Different compile failures point back to different missing details in the original prompt, and recognizing the pattern saves a round trip. This isn't a substitute for reading the actual error, it's a map from symptom to which of the four gaps was probably left open.
| Failure signature | Usually means | Fix on the next prompt |
|---|---|---|
| Unknown method / method does not exist on type | Library version mismatch — the model used an API from a different major version | Paste the exact dependency version from your lockfile |
| Type error on a construct that "should" work | Language or compiler version mismatch, or a strictness setting the model assumed differently than your config | State the exact language version and whether strict mode is on |
| Import or module not found | The model assumed a package structure, export style, or file layout your project doesn't use | Paste your actual import style or a real import line from the file |
| Test fails to compile against the function it's testing | Test framework mismatch, or the test and the implementation were generated against slightly different assumptions | Name the exact testing framework and re-request both together, not separately |
| Works, but errors are swallowed silently in production | No error-handling strategy was specified, so the model picked the quietest option it could get away with | State your error-handling strategy explicitly, as shown in the table above |
The pattern worth internalizing: almost every one of these traces back to a detail that lived in your head or your lockfile, not in the prompt. The model didn't fail to reason. It reasoned correctly from incomplete information, which is a prompting problem with a prompting fix, not a capability ceiling.
It's also worth separating this from a different, more general failure: a prompt that works for a colleague but not for you is often environment-shaped too, but it's usually about differences in tooling, account, or context history rather than the four gaps covered here. If your prompt is genuinely identical to one that worked, and the code you get back is not, check that first before assuming the model changed.
Is This Different From Just Asking for "Better" Prompts in General?
Not structurally, but the stakes are more binary. A weak marketing prompt gets you a mediocre paragraph you can edit. A weak coding prompt gets you something that either runs or it doesn't — there's no "good enough for a first draft" state for a syntax error. That's why the four gaps here (version, dependencies, error handling, tests) are worth memorizing specifically for code, on top of the general instinct to write better prompts that applies everywhere else. If you write prompts across a lot of different domains, from code to marketing copy, the underlying discipline is the same: say what you actually need, not what you assume is obvious. Code just makes it obvious immediately when you didn't.
The honest limit here: no prompt, however specific, replaces reading the code before you run it, or running your own test suite against it. A prompt that compiles is a prompt that got the easy part right. What it does at 2am under real load is a separate question, and it's yours to answer, not the model's.
Stop rewriting prompts. Start shipping.
Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 4.8★ on the Chrome Web Store.
Create An Account