Home Prompting for Code Getting Idiomatic Code on the First Try: Prompts That Constrain the Model

Getting Idiomatic Code on the First Try: Prompts That Constrain the Model

The difference between usable output and a rewrite is usually the constraints you set before the model writes a line.

By Mei Lin, a developer-tools engineer · Published 27 May 2026 · 8 min read · Reviewed against our editorial standards

ADVERTISEMENT

A model that has read most of GitHub can write plausible code for almost any request. Plausible is the problem. Ask a mid-2026 model to "write a function to retry an HTTP call" and you'll get something that works and reads like an average of ten thousand tutorials: its own logging convention, its own idea of what a sensible backoff is, an exception hierarchy that doesn't match yours. It compiles. It also doesn't belong in your codebase.

Getting correct, idiomatic code on the first try is mostly about removing degrees of freedom before generation, not correcting after. Every choice you leave open is a choice the model makes for you, drawn from its training distribution rather than your project. The techniques below are ordered by how much leverage they give per word you spend.

Pin the interface before the implementation

The single highest-leverage move is to hand the model the exact signature, types, and call site you want, and ask it to fill the body. This collapses the space of "correct" answers to roughly one shape.

Weak prompt: "Write a function that parses a duration string like '2h30m' into seconds."

Strong prompt: "Implement this function. Do not change the signature. Return an error for malformed input rather than panicking."

func ParseDuration(s string) (time.Duration, error)
// Accepts combinations of h, m, s (e.g. "2h30m", "45s", "1h").
// Rejects empty string, negative values, and unknown units.

Now the model can't invent a different return type, can't decide to panic, and can't rename things. You've done the design; it does the typing. This is also why generating against a failing test is so effective: the test is the spec, and "make this pass" is unambiguous in a way prose never is.

Give it an example of your house style

Idiomatic doesn't mean idiomatic-in-general. It means idiomatic for your repo. The fastest way to communicate that is to paste one representative function you already like and say "match this." The model is extremely good at few-shot pattern matching; one real example carries more signal than a paragraph of adjectives.

Here is how we write repository methods in this project:

async function getUserById(id: UserId): Promise<Result<User, DbError>> {
  const row = await this.db.query(userByIdSql, [id]);
  if (!row) return err(new NotFound('user', id));
  return ok(mapUser(row));
}

Write getOrgById following the same conventions: Result type,
the err/ok helpers, a mapOrg mapper, no thrown exceptions.

Notice what this rules out: thrown exceptions, nullable returns, a different error shape, a different naming scheme. "Follow the same conventions" backed by a concrete sample beats "write clean idiomatic TypeScript" every time, because the second version lets the model pick which conventions.

State the constraints the code must satisfy, not just the goal

Most first-try failures aren't logic bugs. They're the model doing something reasonable that happens to be forbidden in your context: reaching for a library you don't vendor, allocating on a hot path, using a language feature your target runtime doesn't support. The model can't know these unless you say so.

Keep a short constraint block and reuse it:

Constraints:
- Standard library only, no new dependencies.
- Must run on Node 18 (no top-level await in this file).
- This runs per-request in a hot path; avoid regex compilation inside the function.
- Synchronous; the caller is not async.

Each line closes off a class of wrong-but-plausible output. The Node 18 line alone will save you from a model that defaults to assuming the latest runtime. Constraints are cheap to write and expensive to omit.

Ask for a plan first on anything non-trivial

For a single function, just ask for the code. For anything with branches, edge cases, or cross-file effects, split it: get the approach in prose first, correct it, then ask for code. This matters because a wrong assumption baked into 80 lines is expensive to spot and expensive to fix, while a wrong assumption in a three-bullet plan is a ten-second correction.

Before writing code, list:
1. The cases you'll handle and the cases you'll reject.
2. Any assumption you're making about input shape or ordering.
3. Where this can fail at runtime.
Stop there. I'll confirm before you write the implementation.

The "stop there" is load-bearing. Without it, agentic tools like Claude Code or Cursor's agent mode will happily plan and implement in one shot, which defeats the purpose. Reading a plan takes fifteen seconds and catches the misunderstanding that would otherwise cost you a full review cycle.

Name the failure modes you care about

Models optimize for the happy path unless told otherwise. If concurrency, partial failure, or malformed input matters, say so explicitly and the output changes character immediately.

Compare "write a function to update the cache" against "write a function to update the cache; two requests can call this concurrently for the same key, and a write can fail halfway. Make it safe under both." The second produces locking or compare-and-swap and a rollback path. The first produces a read-modify-write with a race. Same model, same request, completely different code, because you told it what "correct" has to survive.

Prefer editing over generating from scratch

When you already have working code, don't ask for a fresh version. Ask for a diff. "Add pagination to this handler, changing as little as possible, and show me the diff" keeps the model anchored to your existing structure instead of rewriting your handler in its own style and quietly dropping the logging you had. Modern tools surface this as an actual diff view; lean on it. A tight diff is reviewable in a way a 120-line regeneration is not.

What to skip

Two popular moves aren't worth the words in 2026. Role-play preambles ("You are a senior Rust engineer with 20 years of experience") do almost nothing on current code models; the training already covers competent code, and the persona doesn't add constraints. And elaborate politeness or threats don't change output quality. Spend those tokens on a type signature or a real example instead.

One caveat on token budget: everything above adds context, and context isn't free. On a small, well-scoped function you don't need the full apparatus. Reach for signatures, one example, and a constraint block when the cost of a wrong first draft is high, which is most of the time in a real codebase and rarely in a scratch script.

A template you can reuse

Putting it together, a strong first-try prompt for a real task looks like this:

Task: add rate limiting to the /export endpoint.

Match this existing middleware's style: [paste one middleware fn].

Signature: export function rateLimit(opts: RateLimitOpts): Middleware

Constraints:
- Redis is already injected as ctx.redis; use it, don't add a client.
- Sliding window, 100 req / 60s per API key.
- On limit, return 429 with Retry-After. Don't throw.

First, list your approach and any assumptions in 3 bullets.
Stop and wait for my OK before writing code.

None of this is clever. It's just refusing to let the model guess about things you already know. The model is a fast, well-read implementer with no access to your intentions. First-try correctness is the discipline of writing those intentions down before, not after.

promptingcode-generationworkflow

Put this into practice

Compare a flat monthly chat subscription against the equivalent API usage and find the break-even point where one overtakes the other.

Open the Subscription vs API Cost Comparison →

A note on shelf life. AI products change fast. This guide deliberately focuses on the parts that stay true — how to judge a tool, what the trade-offs are — rather than ranking products that will have changed by the time you read it. Prices and feature claims should always be checked against the provider before you rely on them.