When a handler parameter is bound from a query string, how does declaring a default value differ from declaring it nullable?
answer
- absent, empty and unparseable are different
- a default stands in for silence
- conversion failure ignores the default
- an absent marker forces a branch
- changing a default is a contract change
basics
~20 sA default substitutes a value when the parameter is absent, so the handler cannot tell silence from a client sending that same value. A nullable parameter passes the absence through, so the handler branches on it.
solid answer
~40 sBoth declarations say the parameter is optional; they differ in what the handler receives when it is missing. A **default** makes the binder substitute a fixed value, so the handler body has one code path — and loses the information that the client said nothing. A **nullable** parameter hands the handler an explicit *absent* marker, which costs a branch but preserves the distinction, which matters whenever "not specified" and "specified as this value" should behave differently. Two edges catch people out. First, a default covers **absence**, not a failed conversion: if the client sends text the converter rejects, the request is a client error and the default is never reached. Second, absent and **present-but-empty** are not the same thing, and frameworks differ on whether an empty value counts as missing.
go deeper
Know that an optional parameter either gets a stand-in value you declared or arrives empty for you to check, and that you choose which when you write the signature.
Separate absent, empty and unparseable, and explain why a default covers only the first of them while a failed conversion is answered as a client error.
Show the consequences in production: an empty-versus-omitted mismatch between clients, and a default that quietly changed the meaning of every request that omitted the parameter.
Treat defaults as published contract. Decide where the service standardises them, how a change is rolled out and communicated, and when a value is too consequential to have a default at all.
## Three states, not two A query parameter is in one of three states by the time the binder looks at it, and the whole topic follows from keeping them apart. 1. **Absent** — the key does not appear at all. 2. **Present but empty** — the key appears with no characters after the `=`. 3. **Present with text** — the ordinary case, which then either converts or fails. A **default** is a declaration that state 1 (and, in some frameworks, state 2) is replaced by a fixed value. A **nullable** parameter is a declaration that state 1 is passed through to the handler as an explicit absent marker. Neither has anything to do with state 3 failing to convert. ## What each declaration buys | | Default value | Nullable parameter | |---|---|---| | Handler sees on absence | the declared value | an explicit absent marker | | Code paths in the handler | one | two | | Absence distinguishable afterwards | no | yes | | Where the policy is written | the signature, visible with the parameter | the handler body, wherever the branch is | | Typical fit | paging size, sort order, a format flag | a filter, a partial update, anything where "unset" is meaningful | The choice is not stylistic. Ask whether the service needs to behave differently for *unset* than for the value it would have defaulted to. A page size of 20 is a fine default because a client asking for 20 and a client asking for nothing deserve the same answer. A filter is not: "no filter" and "filter by the default" are different requests, and once a default has been substituted the handler has no way to recover which one arrived. ## A default does not rescue a bad value This is the single most commonly mis-stated part of the topic. Conversion runs on whatever text the client actually sent. If that text does not convert, the binder reports a failure and the request is answered as a client error — the default is not consulted, because the parameter was not missing. A default is a stand-in for *silence*, not a fallback for *nonsense*. Treating it the other way round is attractive because it looks robust, and it is how a hand-rolled parse-with-fallback inside a handler usually behaves. It is also how services start silently serving the wrong page of results: the client sends a malformed cursor, the server pretends it sent nothing, and the caller never learns its request was wrong. ## Empty is not absent The second edge is the empty value. A request ending `?sort=` carries the key with no text. Frameworks differ here: some normalise an empty value to absent and apply the default, others hand the empty text to the converter, where it may convert (to an empty string) or fail (for a number or an enumerated value). Neither behaviour is wrong, but a service that does not know which one it has will produce a different result for a form that submits empty fields than for one that omits them — and browsers and client libraries disagree about which they send. The practical rules that follow: - **Decide the empty policy once** and apply it across the service rather than per route. - **Test both spellings** — the omitted key and the empty value — for any parameter with a default. - **Do not express a default by giving the handler a blank value** and checking for it later; that re-creates the ambiguity the declaration was meant to remove. ## How defaults are declared Defaults come in two flavours, and the difference shows up in edge cases: - A **typed default**, given as a value of the parameter's own type, is used as-is; no converter runs on it. - A **textual default**, given as a string that the binder feeds through the same converter as a client-supplied value, which means a mistyped default can fail at startup or on first use, and means the default follows the same acceptance rules as real input. Either way the default belongs to the *declaration*, not to the caller. It is documented API surface: changing it changes the response to every request that omits the parameter, silently, with no client-visible error. That makes a default change a contract change, and worth the same care as renaming the parameter. ## What good looks like Declare a default when the absent case has an obvious, stable, documented meaning, and make that meaning the same in every handler that takes the parameter. Declare it nullable when the handler genuinely needs the distinction, and then branch explicitly rather than comparing against a sentinel value the client could also have sent.
- A client sends a value that fails conversion for a parameter that has a default. What should happen?The request is answered as a client error. The parameter was present, so the absence rule that the default expresses never applies; the converter simply could not produce a value. Falling back to the default here hides a caller's bug and returns an answer to a question nobody asked.
- When is a nullable parameter clearly the right choice over a default?When "unset" has to behave differently from any value the client could send — an optional filter, a field in a partial update, a flag that inherits from configuration when not given. A default collapses those into one case, and no later code can recover the difference.
- How do you keep an empty parameter value from behaving differently across routes?Decide one policy — empty means absent, or empty is text the converter judges — and enforce it in shared binder configuration rather than per handler. Then test both an omitted key and an empty value for every optional parameter, because clients and browsers disagree about which they send.
A paper form with a pre-printed "20" in a box versus one left blank: once the pre-printed box is handed on, nobody can tell whether the applicant wrote 20 or simply did not answer.
saying these in an interview costs you the question
- Expecting a default to stand in for a value that failed conversion
- Assuming every framework treats an empty value as an absent one
- Believing the handler can still tell an omission from an explicitly sent default
- Declaring a parameter nullable and then never branching on the absent case
- Changing a default and calling it an internal detail rather than an API change