A configuration chain runs without error but nothing appears — the closing call was never written. How would you design that away?
answer
- compiles, runs, does nothing
- a discarded expression is legal
- nothing applied until the close
- types can forbid the omission
- partial type is not the result type
basics
~20 sNothing catches it because a chain of calls is an ordinary expression whose value may be discarded. The fix is to make the incomplete chain a different type from the result the surrounding code needs, so only the closing call can produce what the caller must supply.
solid answer
~50 sThe silence is structural: the steps only recorded state, and the whole chain is an expression, so discarding its value is legal. Three defences exist, in increasing strength. Weakest, **documentation and review** — it fails for the same reason all conventions fail. Middle, a **run-time signal**: have the receiver complain if it is discarded unused, or have a test assert the configuration is non-empty. Strongest, **make it unrepresentable in the types**: each configuring step returns a partial type that the surrounding code cannot use, and the closing call is the only call that returns the result type it demands. Then omitting the close is a type error at the boundary rather than a missing route at run time. The cost is a wider API surface and a chain that can no longer be left half-written on purpose.
code
pseudocode · 12 lines// silent: the chain is built and dropped
function install(table):
table.route("/orders").method("GET").handler(show)
return table // no entry was ever added
// unrepresentable: handler(...) yields PartialRoute,
// and only close() yields Route, which the caller demands
function install2(table) returns Route:
return table.route("/orders")
.method("GET")
.handler(show)
.close()go deeper
Recall that configuring steps only record state, so a chain without its closing call builds a description that is thrown away and produces no visible error.
Explain why the expression is legal and its value discardable, and describe how distinct return types per phase turn the omission into a type error.
Diagnose the live version: a route that is absent rather than broken. Then argue what you would change, and be honest that types force the call to be written, not that its values are correct.
Decide the standard for a library many teams call: unrepresentable-by-type where a result is consumed, loud development diagnostics where the chain exists for its effect, and accept the wider API that buys.
## The defect: a chain that compiles, runs, and does nothing This is the failure interviewers reach for first when they hear "fluent API". Somebody writes a perfectly readable chain — open an entry, set a method, attach a handler — and the entry never appears. Nothing threw, nothing logged, and the source looks exactly like the four lines above it that do work. The cause is the mechanism itself. A configuring step **records** state and returns a receiver; it does not **apply** anything. The closing call is what applies it. And since a chain of calls is an ordinary expression, a language is entirely within its rights to evaluate it and drop the result on the floor. ## Why the compiler is usually silent - **The expression is well formed.** Every call exists and every argument type checks; the only thing missing is a call nobody was obliged to write. - **Discarding a value is normal.** Plenty of legitimate calls return something no caller wants. A blanket rule against unused results would reject far more good code than bad. - **The chain has a type either way.** The half-built receiver is a perfectly ordinary value that may be stored, passed and returned, so nothing about it says "unfinished". - **The effect is absence, not error.** A missing route is a route that does not match — indistinguishable at a glance from one that was never meant to exist. One qualification worth stating in an interview: this is the common case, not a law. If a step eagerly applies part of its effect, an omitted close leaves a *partial* configuration instead of none, which is harder to diagnose than either extreme. ## Three places to catch it | Defence | When it fires | What it costs | |---|---|---| | Convention and review | when a human notices | nothing, and it fails like every convention | | Run-time signal on a discarded receiver | after the code runs, near the mistake | a check, plus the noise of deliberately abandoned chains | | Result type only the closing call can produce | while the code is being written | a wider API, and half-written chains become illegal | The third one is the design answer, and it works by a single move: **stop letting the incomplete thing stand where the complete thing is expected.** 1. The origin call returns a *partial* type, not the result type. 2. Every configuring step also returns a partial type — possibly the same one. 3. Only the closing call returns the result type. 4. The surrounding code is written to demand the result type: a registration function takes it, a field holds it, the enclosing function returns it. Now the forgotten close is not a silent absence; it is code that does not fit where it was written. Note precisely what the types do and do not do here: they force the closing call to be **written**. Whether the values it accumulated are coherent is still checked when that call actually runs — the type discipline moves the *omission* to compile time, not the validation. ## When the type-level defence is not available Sometimes the chain is a statement on purpose — its whole job is a side effect, and no caller wants the result. Then there is no surrounding demand for the result type and step 4 above has nothing to hook into. Options in that case: - **Make the closing call the thing that has the effect**, and give the chain no other way to matter. The result type may be trivial; what matters is that no step registers anything on its own. - **Signal a discarded partial receiver** — have the receiver notice it was abandoned without being closed and report it loudly in development. - **Test the outcome, not the call.** Assert that the configuration contains what the chain claimed to add. This catches the omission regardless of how the API is shaped, and it is the defence that survives an API redesign. ## The trade-off to state out loud Making the omission unrepresentable is not free: - The API grows an extra type per distinct phase, and each has to be named and documented. - A partial chain can no longer be built, stashed and finished later, which some callers legitimately want. - The types describe *structure*, so any rule about values still has to be enforced when the closing call runs. A reasonable position, and a good thing to say in an interview: encode the closing call in the types when the chain produces a value someone must use, and fall back to a loud run-time signal plus a test when the chain exists purely for its effect.
- What does a chain whose first step applies its effect eagerly give up?Whole-configuration validation and atomicity. If the entry is registered before the later steps refine it, an incomplete chain leaves a half-configured entry live rather than nothing, and no single call ever sees the configuration as a whole to check it. The failure gets harder to diagnose, not easier.
- How would you catch the omission when the types cannot forbid it?Two cheap layers. Have the partial receiver notice it was abandoned without being closed and report that loudly in development builds. Then assert the outcome in a test — that the configuration actually contains the entry the chain describes — which keeps working however the API is later reshaped.
- Is a run-time complaint on an unclosed chain always appropriate?No. Some callers build a partial chain deliberately and discard it — an experiment, a branch not taken, a template that is never finished. A loud complaint turns legitimate use into noise, so it belongs in development diagnostics rather than in the production path.
A form filled in on screen changes nothing until submit is pressed. Every keystroke was real; the record simply never existed.
saying these in an interview costs you the question
- Expects the compiler to reject a chain missing its close
- Thinks the omission raises an error at run time
- Believes the configuring steps already registered something
- Says one uniform chain type can make the omission unrepresentable
- Claims documentation is a sufficient defence