A gRPC handler rejects a request. How do you choose between INVALID_ARGUMENT, FAILED_PRECONDITION, NOT_FOUND and UNIMPLEMENTED?
answer
- the number is the machine-readable part
- request wrong, or world wrong?
- missing entity versus missing method
- credential absent versus caller not allowed
- INVALID_ARGUMENT (3) ignores state, FAILED_PRECONDITION (9) is state
basics
~20 sAsk what is wrong. The arguments themselves give INVALID_ARGUMENT (3); well-formed arguments blocked by system state give FAILED_PRECONDITION (9); a named entity that does not exist gives NOT_FOUND (5); a method this server does not implement gives UNIMPLEMENTED (12).
solid answer
~40 sThe status code is the only machine-readable part of a failure, so it has to say **what kind** of failure it was. `INVALID_ARGUMENT (3)` means the request is wrong **whatever the system state** — a malformed identifier, a field out of shape. `FAILED_PRECONDITION (9)` means the request is fine but the **current state** forbids it: at a pharmacy counter, a valid prescription whose plan requires a prior authorisation that is not on file. `NOT_FOUND (5)` means the **entity named** does not exist — no prescription with that id. `UNIMPLEMENTED (12)` means **this server does not implement the method** at all, which is a deployment or contract fact, not an answer about the caller's data. The test that separates the first two is simple: would fixing the request help, or only fixing the state?
go deeper
Learn that gRPC failures are numbered codes from a fixed enum, and that returning a specific one instead of a generic error is the whole point. Start with NOT_FOUND, INVALID_ARGUMENT and UNAUTHENTICATED.
Be able to separate adjacent pairs on demand: argument versus state, missing entity versus missing method, unidentified caller versus unauthorised caller. Interviewers ask for the discriminator, not the list.
Show the operational consequence. Explain how a service that mislabels business rejections as INTERNAL produces an error-rate floor nobody can act on, and how you would audit an existing handler's code choices.
Status semantics are an interface that outlives the code behind it. Decide how codes are agreed across teams and what it costs to change one after callers already branch on it.
## Why the code, and not the message, carries the meaning A failed gRPC call returns a numeric status from a seventeen-value enum, and that number is the only part a caller can branch on without reading English. Choosing it badly is not a cosmetic problem: callers, dashboards and operators all read it, and a service that returns `INTERNAL (13)` for every rejection has told them nothing except that something went wrong somewhere. The pharmacy counter makes the distinctions concrete. A prescription is scanned and one eligibility call goes out, behind which sit a formulary lookup, a plan-coverage lookup and a prior-authorisation lookup. Each of the four codes below describes a genuinely different thing the pharmacist should be told. ## Argument or state: INVALID_ARGUMENT versus FAILED_PRECONDITION This is the pair interviewers actually probe, and the discriminator is whether the *system's* state is part of the story. - **`INVALID_ARGUMENT (3)`** — the request is wrong **independently of state**. The prescription id is not a prescription id at all; a required field is empty; a quantity is negative. Re-sending it unchanged at any future moment fails identically. - **`FAILED_PRECONDITION (9)`** — the request is **well-formed** and the failure is about **current state**. The prescription is real, the plan is real, and the plan requires a prior authorisation that is not on file. Nothing about the request needs to change; something about the world does. The test in one line: *would fixing the request help, or only fixing the state?* A nearby refinement is **`OUT_OF_RANGE (11)`**: an argument that is wrong specifically because it falls outside a range the caller could have known from state — reading past the end of a collection, for instance. It is a more informative sibling of `INVALID_ARGUMENT`, not a replacement for it. ## Missing thing or missing method: NOT_FOUND versus UNIMPLEMENTED - **`NOT_FOUND (5)`** is about **data**: the method ran, looked for the entity the request named, and there is no such entity. No prescription carries that id. - **`UNIMPLEMENTED (12)`** is about the **contract**: this server does not implement, or does not support, the method that was called. The counter software was built against a newer schema than the deployed server, or the method exists but is disabled here. They feel similar because both are 'that isn't here', but they point at different fixes: one is a data question for the caller, the other is a deployment or versioning question for whoever owns the service. A service that answers `NOT_FOUND` when a method is missing sends people to look in the database for something that was never a database problem. ## The two pairs that come up in the same breath | pair | choose the first when | choose the second when | |---|---|---| | `UNAUTHENTICATED (16)` / `PERMISSION_DENIED (7)` | no valid credential identified the caller | the caller is identified, and is not allowed to do this | | `UNAVAILABLE (14)` / `INTERNAL (13)` | the service currently cannot serve the call | an invariant the code relies on was broken | Two rules go with that table. `PERMISSION_DENIED` **must not** be used for a missing or unusable credential — that is what `UNAUTHENTICATED` is for. And `PERMISSION_DENIED` is not the code for running out of quota or capacity; `RESOURCE_EXHAUSTED (8)` is. ## A workable decision order 1. Could the server not tell **who** is calling? `UNAUTHENTICATED (16)`. 2. Does it know who, and this caller may **not** do this? `PERMISSION_DENIED (7)`. 3. Does this server not implement the **method**? `UNIMPLEMENTED (12)`. 4. Is the **request itself** wrong regardless of state? `INVALID_ARGUMENT (3)`, or `OUT_OF_RANGE (11)` where that is more precise. 5. Does the **named entity** not exist? `NOT_FOUND (5)`. 6. Is the request fine but the **state** wrong? `FAILED_PRECONDITION (9)`. 7. Is the service simply **not able to serve** right now? `UNAVAILABLE (14)`. 8. Did an assumption inside the code **break**? `INTERNAL (13)`. Work down the list and stop at the first match; the ordering puts the narrow, informative codes ahead of the broad ones on purpose. ## The codes you should reach for least `UNKNOWN (2)` exists for genuinely unclassifiable failures, and using it as a default converts a rich enum into a single bit. `INTERNAL (13)` is a confession of a bug, so a handler that returns it for an expected business rejection is mislabelling ordinary operation as breakage — and that mislabelling shows up as a permanent error-rate floor that nobody can act on. What a caller **does** with the code it receives is a separate subject from how the code is chosen; here the responsibility is to make the choice honest, so that whatever reads it downstream has something true to work with.
- What separates gRPC's UNAVAILABLE (14) from INTERNAL (13) when a handler blows up?`UNAVAILABLE` says the service currently cannot serve the call — a condition of the moment. `INTERNAL` says an invariant the code depends on was broken, which is a defect. Returning `INTERNAL` for an overloaded dependency misreports a condition as a bug and buries the real one.
- Why must PERMISSION_DENIED (7) not be returned for a missing credential in gRPC?Because `UNAUTHENTICATED (16)` is the code for 'no valid credential identified the caller'. Collapsing the two tells the caller it is forbidden when in fact it was never recognised, which points at an authorisation policy instead of the credential that actually needs attention.
- Where does OUT_OF_RANGE (11) fit next to INVALID_ARGUMENT (3)?It is the more precise sibling for an argument that is wrong because it falls outside a range the caller could have deduced from state — reading past the end of a collection, for example. `INVALID_ARGUMENT` stays correct for arguments that are wrong regardless of state.
saying these in an interview costs you the question
- Returns INTERNAL (13) for every rejection the handler makes
- Uses NOT_FOUND (5) when the method itself is missing
- Uses PERMISSION_DENIED (7) for a missing or invalid credential
- Thinks FAILED_PRECONDITION (9) means a malformed argument
- Treats UNKNOWN (2) as a safe default for all errors
- Maps every rejection onto an HTTP status code instead