In this guide
Every engineer asks questions about code many times a day: in chat, in code review, in a standup, in an AI tool. Some get a precise answer in two minutes. Others start a thread that runs for an hour, ends in a call, and still leaves the asker unsure. The difference is rarely the person answering. It is almost always the question.
A good question about code is specific about the goal, anchored to concrete code or behavior, honest about what you already tried, and shaped so the answer can be checked. That structure works on senior teammates and on AI tools alike, for slightly different reasons. This post lays out the structure, gives templates for common situations, and covers the mistakes that make questions slow to answer. It is a companion to how to read an unfamiliar codebase: reading generates questions, and asking them well is how you get unstuck fast.
Key takeaways
- Lead with the goal, not just the immediate problem. "I want X; I tried Y; Y fails because Z" avoids the XY problem.
- Anchor to specifics: file paths, line numbers, function names, exact error messages, request IDs, commit hashes.
- Say what you already tried and what you ruled out. It saves the answerer from repeating it.
- Ask a question with a checkable answer: "where", "which", "what happens when", rather than "how does this work".
- For AI tools, the same structure applies, plus explicit scope and a request for references so you can verify.
- Close the loop: post the answer you found and update docs if the question will come up again.
Why questions about code go wrong
Three patterns account for most slow threads.
The XY problem. You want to do X, you decide Y is the way, you get stuck on Y, and you ask about Y. The answerer helps with Y, which was never the right approach. The fix is to always state X.
Missing context. "Why is the build failing?" with no error, no branch, no command. The answerer has to ask three follow-up questions before they can start thinking, and in an async channel each round trip can take an hour.
Unbounded scope. "How does auth work?" has no stopping point. The answerer either writes an essay or gives a vague summary. Neither unblocks you.
A structure that works
A good question has five parts. Not every question needs all five, but checking against the list catches most problems.
- Goal. What you are ultimately trying to do, in one sentence.
- Context. Where you are: the file, function, endpoint, branch, environment.
- Observation. What you see: exact error, actual versus expected behavior, a minimal reproduction.
- Attempts. What you tried and what you learned from each attempt.
- The question. One specific question, ideally with a checkable answer.
Here is a vague question:
Hey, the invite thing isn't working, any ideas?
And the same question with structure:
Goal: let project editors (not just admins) send invites, for TICKET-812.
Context: POST /api/projects/:id/invites, route in src/routes/projects.ts:31.
I changed requireRole('admin') to requireRole('editor') on my branch invites-editors.
Observation: editors now get 201 locally, but the invite email never arrives.
Admins still get the email. No error in the API logs.
Tried: checked the queue dashboard; the send-invite-email job is created for both.
For editors the job fails with "inviter lacks permission" in workers/email.ts:44.
Question: is there a second permission check in the email worker on purpose,
and if so, where is the source of truth for who can invite?
The second version takes two minutes longer to write and probably saves an hour. It also makes it obvious that the asker did real work, which makes people more willing to help. Often, writing the structured version leads you to the answer yourself before you hit send.
Make the answer checkable
Prefer questions whose answers you can verify:
| Hard to check | Checkable |
|---|---|
| How does caching work here? | Where is the TTL for the product cache set, and is it per key or global? |
| Is this code safe? | Can userId in deleteAccount ever come from request input rather than the session? |
| Why is it slow? | Which query in the dashboard endpoint takes most of the 2s? Here is the trace. |
| What does this service do? | Which other services call inventory-svc, and over what protocol? |
| Should I refactor this? | If I extract computeDiscount into its own module, will anything outside pricing/ break? |
Checkable questions get shorter answers, and you can confirm them yourself with "find references", a debugger or a quick test. They also tend to have a definite owner, which helps you route the question to the right person.
Asking people
Choose the right channel and person
- Team channel for questions others might also have, or when you do not know who owns the area. Answers in public help the next person who searches.
- Direct message to a specific owner when the question is narrow and you know who knows.
git logandgit blameon the file tell you who has worked on it recently. - Code review comment for questions about a specific diff, anchored to the line.
- Issue or ticket for questions that reveal a bug or a missing decision.
# Who has changed this file most recently
git log --format='%an %ar %s' -10 -- workers/email.ts
Respect the answerer's time
- Put everything in one message. "Hi" followed by a pause makes the other person wait for the actual question.
- Include code as text with file paths, not as a screenshot. Text can be searched and copied.
- Link instead of describing: a link to the line in the repository, the CI run, the trace or the log query.
- Say how urgent it is. "Blocking a release today" and "curious, no rush" get different responses, appropriately.
When to ask
A common rule of thumb is to ask after 30 to 60 minutes of being stuck, once you have tried the obvious things. New team members tend to wait too long; ask sooner in your first weeks, since much of what blocks you is unwritten context nobody could reasonably guess.
Asking AI tools
AI coding tools and assistants respond to the same structure, for a different reason: they have no shared context with you unless you supply it or the tool retrieves it. A vague question gets a generic answer drawn from how code like yours usually works, which may not be how your code works.
Scope explicitly
Tell the tool what part of the codebase the question concerns and what it should not assume:
In this repo, focus on src/routes/projects.ts and workers/email.ts.
Question: when an invite is created by a user with role "editor",
what permission checks run between the HTTP request and the email being sent?
List each check with file and line. If you are unsure whether a check
applies, say so instead of guessing.
Ask for references, then verify them
Ask for file paths, line numbers and function names in the answer. References let you check the claims in seconds. If a referenced function does not exist, you have caught a hallucination before it cost you anything. The causes and fixes for that failure mode are covered in why AI agents hallucinate APIs.
Prefer questions about this code over general knowledge
"How do permission checks usually work in Express apps?" invites a textbook answer. "Which permission checks run on this route in this repo?" forces the tool to look at your code, and it is the question you actually need answered. Tools that index the repository structurally, rather than relying on general training, answer the second kind much better; the reasons are discussed in how AI coding agents understand a codebase.
Follow up narrowly
When the first answer is close but not quite right, do not restart with a new broad question. Ask a narrow follow-up anchored to the answer: "In step 3 you said the worker checks canInvite. Where is canInvite defined, and does it read the project role or the org role?"
Templates
Bug or unexpected behavior
Goal:
Where: (file/endpoint/branch/env)
Expected:
Actual: (exact error or output)
Repro: (steps or command)
Tried:
Question:
Understanding a design decision
I'm working on (task) in (file/module).
I noticed (specific thing) at (file:line), which seems to (your reading).
Before I change it: is this intentional, and is there a constraint I'm missing?
(git blame points to commit abc123, "fix race", which didn't explain it.)
Before a change with wide impact
I want to change (function/type) in (file) to (new behavior).
I found these callers: (list).
Question: are there callers outside this repo, or behavior that depends on the old (detail)?
Close the loop
When you get an answer, finish the thread:
- Post what you found in the same thread, especially if you solved it yourself. Search engines and coworkers find threads with answers; threads that end at "nvm, fixed it" help nobody.
- Turn recurring answers into docs. If you had to ask, the next person will too. A comment next to the surprising code or a line in the module README is enough.
- Thank the person and, when relevant, mention what the answer let you do. It makes them more likely to help next time.
Questions as a reading tool
Asking well is not only for when you are stuck. Writing down questions as you read is one of the best ways to understand a codebase: each question marks a gap in your model. Some you can answer yourself with "find references" or a debugger; others become the agenda for a short conversation with a senior engineer.
For questions you want answered directly from the code, OpenVisio's Ask takes a question about a repository and returns a step-by-step explanation with the relevant code, a flowchart or the architecture shown beside each section, and follow-up questions stay docked under the answer. The structure in this post still applies: a scoped, checkable question gets an answer you can verify against the code in front of you.
Good questions are a habit. State the goal, anchor to specifics, say what you tried, ask for something checkable, and close the loop. People and tools will both answer you faster and better.
