Why must every constructor, operator, and pure function return be captured?
← Code Quality · Ref: Q732
EK9 requires that return values from constructors, computational operators, and pure functions are always captured. There is no threshold: discarding any of these is always an error.
THIS IS A DESIGN RULE, NOT A THRESHOLD
Unlike nesting depth or inheritance depth, this check has no numeric boundary. It is binary: either the return is captured or it is not.
WHY EACH CATEGORY IS ENFORCED
CONSTRUCTOR RETURNS (E11055)
A constructor creates an object. Calling it as a bare statement means the object is immediately garbage collected. This is always either a bug (forgot the assignment) or a misuse.
OPERATOR RETURNS (E11050)
Computational operators (+, -, *, /) produce new values without modifying the original. Discarding the result means the computation was wasted work.
PURE FUNCTION RETURNS (E11051)
Pure functions have no side effects. If the return is discarded, the entire call did nothing observable. This is dead code.
RESULT/OPTIONAL RETURNS (E11052)
Result and Optional types exist to force error handling. Discarding them defeats their purpose, like catching an exception and silently ignoring it.
WHEN RETURNS CAN BE IGNORED
Impure functions CAN be called for their side effects:
stdout.println("hello") // Impure: called for side effect, return (Void) is fine to ignore list += item // Mutation operator: modifies list in place
See Q316 for discarded returns overview. See Q693 for captured operator returns. See Q310 for code quality overview.
Example
defines module qa.codequality.discardedreturnboundary defines function <?- Pure function: return value must always be captured. -?> sanitizeInput() as pure -> rawInput as String <- cleaned as String: rawInput.trim() <?- Pure function: validates input length. -?> checkLength() as pure -> inputText as String <- isValid as Boolean: length inputText > 0 defines class <?- Simple sensor class demonstrating constructor capture. -?> Sensor reading <- Float() Sensor() -> reading as Float this.reading: reading getReading() as pure <- rtn as Float: reading default operator ? defines program DiscardedReturnBoundaryDemo() stdout <- Stdout() //Correct: constructor return captured temperatureSensor <- Sensor(98.6) stdout.println($temperatureSensor.getReading()) //Correct: pure function return captured trimmedInput <- sanitizeInput(" hello ") stdout.println(trimmedInput) //Correct: pure function return captured (independent variable) isValid <- checkLength("test") stdout.println(`Valid: ${isValid}`) //Impure: println called for side effect, OK to not capture stdout.println("done")
Common mistakes
E11055 — Constructor return discarded. The Sensor object is created then immediately garbage collected. Assign it to a variable. See ek9 -h E11055 for details.
Incorrect:
Sensor(98.6) stdout.println("98.6")
Correct:
temperatureSensor <- Sensor(98.6) stdout.println($temperatureSensor.getReading())
E11051 — Pure function sanitizeInput has no side effects. Discarding its return value makes the call completely useless. See ek9 -h E11051 for details.
Incorrect:
sanitizeInput(" hello ") stdout.println("hello")
Correct:
trimmedInput <- sanitizeInput(" hello ") stdout.println(trimmedInput)
Other ways to ask this
- What triggers E11050 discarded operator return?
- What triggers E11055 discarded constructor return?
- What triggers E11051 discarded pure function return?
- When can I ignore a function return value?
Coming from another language?
Java: all return values silently discardable. Rust: #[must_use] attribute (opt-in, suppressible). Go: blank identifier _ explicitly discards. Python: no enforcement. C++: [[nodiscard]] attribute (opt-in). EK9: automatic enforcement for all constructors, pure functions, computational operators, and Result/Optional returns.
Keywords: dead, operator, boundary, discard, quality, constructor, capture, return, pure, E11055, code, E11052, clean-code, E11051, E11050