Nullability and Return Types
A method’s return type answers two independent questions: can this fail? (bare vs. Result<T>) and can the value be legitimately absent? (non-null vs. nullable). Decide them separately, then combine.
The return-type rules
Section titled “The return-type rules”Wrapping in Result<T>:
- Bare type when the call cannot fail in any way, or when it is an initialization method that is only called during component initialization, where exceptions are allowed (see Errors and Exceptions).
- Wrapped in every other case, unless explicitly overridden by the author.
Nullability:
- Nullable
T?when the method’s only responsibility is to retrieve a value that may be legitimately absent. - Non-nullable when the value must always be present, or when the method’s responsibility is to act on the value before returning it (an updating method, for example). In that case the method returns
Result<T>with a non-nullable parameter, capturingNotFoundas one of the possible failures.
A bare return is a claim, and the claim must be structurally true
Section titled “A bare return is a claim, and the claim must be structurally true”“Cannot fail in any way” is not a judgement about how the method is called today; it is a property the type system must guarantee. A method whose body contains an unchecked cast, an array index, or a require can fail, whatever its callers currently do.
The usual way to earn a bare return is to move the invariant into construction — see smart constructors. Once the only way to build an input is through a validating factory, a method over that input genuinely cannot fail, and its bare return becomes legal rather than merely tolerated.
Retrieval, deserialization, and acting on a value are three different things
Section titled “Retrieval, deserialization, and acting on a value are three different things”The nullable branch above is for retrieval. Two neighbouring cases are routinely misfiled into it:
- Deserialization is not retrieval. Parsing untrusted wire input — a scope name, an enum case, a date — yields
Result<T>, notT?. A string that names nothing is malformed input, not an absent value, and the caller needs the reason. This also keeps a parser on the same error channel as the codecs around it: a payload parsed half throughT?and half throughResult<T>forces every caller to handle two failure shapes for one wire message. - Acting on a value is not retrieval. If the method mutates, persists, or otherwise does something to the value on the way through, absence is a failure of that operation, not a legitimate answer. Return
Result<T>carryingAppError.NotFound.
Worked
Section titled “Worked”// Pure retrieval, legitimately absent → bare nullable.fun lookup(key: String): SettingsDefinition<*>? = byKey[key]
// Retrieval where a *different* condition is distinguishable from absence → Result<T?>.suspend fun childOf(parentId: EntityId): Result<Child?> // failure = parent missing // success(null) = parent has no child
// Deserialization → Result<T>. "unknown" is malformed input, not absence.fun fromWire(value: String): Result<SettingsScope>
// Acts on the value → Result<T>, NotFound as a failure.suspend fun activate(id: EntityId): Result<Subscription>
// Cannot fail — the paired factory guarantees the type — → bare.fun encode(value: Any?): JsonElementNull Value Semantics
Section titled “Null Value Semantics”-
Null should only be returned from methods that explicitly search or retrieve information. In these cases the method returns
Result<T?>:- A
nullpayload means the value was not found. - A
failuremeans a different condition occurred (e.g., a required parent record was itself missing).
Example: retrieving a child record from a parent-child relationship — if the parent is null, return
failure. If the parent is found but the child is not, returnsuccesswith a null payload. - A
-
In all other cases (e.g., mutating system state), if the target of the operation is null or not found, return
Result.failure(...).
Result<T?>or bareT?? Rule 1’sResult<T?>is for retrieval where a second condition is distinguishable from absence — the parent-child example, where a missing parent and a missing child are different answers. When absence is the only thing that can happen and nothing else can go wrong, there is no failure channel to model and a bareT?is correct. Do not wrap a total lookup in aResultthat can never be a failure; every caller then pays for a branch that cannot be taken.
Related
Section titled “Related”- Errors and Exceptions — what goes in the failure channel, and when a bootstrap validator may throw instead.
- Functions and Result Handling — composing the
Results these rules produce. - Construction and Typed Values — how to earn a bare return.
- Kotlin Coding Standards — index.
Copyright: © Arda Systems 2025-2026, All rights reserved