What has happened when a source declared to carry at most one value completes without emitting a value?
answer
- count the possible endings
- three outcomes, not two
- absence is an answer
- empty is not a failure
- completion carrying zero values
basics
~20 sNothing was found and nothing failed: empty completion is a third outcome beside a value and a failure. The caller must decide what absence means here - a default, a fallback source, or an error it raises itself.
solid answer
~40 sThe source ran to its end and had nothing to report. Because the type declares **at most one** value, zero is inside its contract, so a subscriber can observe three distinct endings: a value then completion, completion with no value, or a failure carrying a reason. Empty completion is a successful operation with an empty result - a directory lookup for an identifier nobody holds is not an outage. The caller owes that case an explicit decision: substitute a default, fall back to another source, or convert the absence into a failure at the layer that knows absence is illegal. The accidental fourth option - handling only the value and the failure - is the bug: the sequence ends, and the work that was supposed to happen simply never happens.
code
pseudocode · 8 lines// directory lookup by identifier: declared to carry at most one record
lookupById(id)
.onValue(record -> respondWith(record))
.onEmpty(() -> respondNotFound(id)) // the branch people forget
.onFailure(reason -> respondUnavailable(reason))
// without the middle branch the sequence still ends normally
// and nothing at all is written back to the callergo deeper
Remember that a source carrying at most one value has three endings, not two: a value, nothing at all, or a failure. Name the empty ending out loud and say what your code does in that case.
Explain why absence is a successful outcome rather than an error, and show the three deliberate responses - default, fallback source, or an explicit conversion to a failure - plus the symptom when the case is unhandled.
Show where in a real service you place the absence decision, how you keep not-found out of the failure rate that drives alerting, and how you stop a swallowed empty from silently shortening a batch result.
Set the convention for the codebase: which layer owns turning absence into an error, whether absence is ever encoded in the payload, and how APIs signal not-found consistently so teams stop inventing per-service answers.
## Three endings, not two A source that pushes values to a subscriber ends in exactly one of two ways: it **completes** or it **fails**. Before ending, it may emit values. Cross those two facts for a source that declares it carries **at most one value**, and a subscriber can observe three distinguishable outcomes: - **A value, then completion.** The ordinary success path. - **Completion with no value at all.** The source ran to its end and had nothing to report. - **A failure.** The source could not do its job, and carries a reason why. The middle outcome is the one callers forget, because an ordinary function call offers only two shapes: it returns something, or it raises. A source with a declared cardinality of *at most one* has a third shape, and the declaration is exactly what puts it there - **at most one** means zero is inside the contract. ## Empty is an answer; a failure is the absence of one Take a directory service with a lookup by identifier. Somebody asks for an identifier nobody holds. The store was reachable, the query ran, the index was consulted, and the answer came back: no such record exists. That is a *successful* operation whose result happens to be empty. Contrast it with the store being unreachable: there is no answer at all, only a reason the question could not be settled. Collapsing the first into the second costs three concrete things: - it erases the distinction between the common expected case and an incident, so error rates and alerts stop carrying information; - it invites retries of something that will never change, because a record that does not exist stays non-existent however often you ask; - it destroys the caller's ability to answer differently, and *not found* and *temporarily unavailable* are different answers to whoever is waiting. The reverse mistake is just as damaging: treating a failure as an empty result hides an outage behind a page that cheerfully reports that nothing matched. ## What the caller has to decide Every subscriber to an at-most-one source makes one of these choices, and a good one makes it deliberately: 1. **Absence is normal here, so supply a value.** Substitute a default, or fall back to a second source that may know the answer. 2. **Absence violates a rule this layer owns, so turn it into a failure explicitly.** Convert the empty completion into a failure signal carrying a reason, at the boundary that actually knows the rule. 3. **Absence is not this layer's business, so pass it on.** Keep the at-most-one shape and let the edge of the system decide what it means. What is never acceptable is the accidental fourth option: registering handling for the value and handling for the failure and nothing for the empty completion. The terminal signal then arrives, the pipeline finishes, and the work that was supposed to happen never happens - no value, no error, often no log line. The symptom is a request that produces nothing and a trace that looks perfectly healthy. ## The outcomes side by side | Outcome | What the subscriber observes | What it means | Sane handling | |---|---|---|---| | Value then completion | one value, then a terminal completion | the thing exists | use it | | Empty completion | a terminal completion, zero values | the thing does not exist | default, fallback, or an explicit not-found response | | Failure | a terminal failure carrying a reason | the question could not be answered | surface it, retry it, or degrade | ## Why the type cannot make the decision for you A declaration of *at most one* places zero inside the space of legal results, so the caller, not the type, owes the empty case an answer. Only a type that declared **exactly one** would let a caller skip the empty branch, and a source that has to go and find something can rarely make that promise: the lookup either finds a record or does not. Ecosystems differ in how they surface the case - some give the empty ending its own callback, some fold it into a general completion callback, and some hand the caller a value that may be absent - and what does not differ is that the case exists and must be handled. Notice also what empty completion is *not*. It is not a value that happens to be blank, and modelling it as a blank placeholder is a trap: downstream code then cannot tell a record that genuinely contains empty fields from a record that was never there. Keep absence structural rather than encoding it in the payload. ## Where this is most expensive The damage scales with how far the empty case travels before anyone notices. An empty completion swallowed at the edge costs one blank response. The same empty completion swallowed in the middle of a batch job costs a silently short result set that nobody reconciles for weeks, because every stage downstream is working correctly on the values it did receive. Decide what absence means as close to the source as the business rule allows, and make the decision visible in the code rather than implied by the handlers you happened to write.
- If the surrounding domain rule says a record must exist, where should the empty case become a failure?At the boundary that owns the rule, and explicitly. Convert the empty completion into a failure signal carrying a reason such as *no record for this identifier*, so everything downstream sees a described failure rather than a silent absence. Do not push the conversion into the source itself - other callers may legitimately expect nothing.
- Does the empty case disappear when the source is declared many-valued instead?No, it changes shape. A many-valued source that matches nothing still completes with zero values, so the caller still has a zero case. It only turns into a value once you collect the sequence into a single collected result, where absence becomes an empty collection - a value the caller must still inspect.
- Why is substituting a blank placeholder record for the empty case usually worse than handling it?Because it makes absence indistinguishable from presence with empty fields. Downstream stages see a value and behave as though a record was found, so the miss is laundered into a false positive. If a substitute is genuinely wanted, make it an explicit marker whose meaning is *not found*, not a hollow copy of the real thing.
Asking a receptionist for a guest by room number: being told nobody is registered under it is a real answer, not the phone line going dead.
saying these in an interview costs you the question
- Treats an empty result as a failure signal to be retried
- Assumes a declared at-most-one source always emits exactly one value
- Registers value and failure handling only, leaving completion unhandled
- Substitutes a blank placeholder record so something is always emitted
- Says completion proves a value already arrived
- Alerts on not-found at the same severity as an unreachable store