Build1 publisher3 min readPublished
Returning Unit is a promise to tell you nothing about how the call can fail
A JetBrains Kotlin post argues that expected business failures belong in the return type, not the implementation. The payoff is that error handling becomes reviewable.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- A JetBrains Kotlin blog post titled "Signatures, be true: domain errors and functional handling in Kotlin" opens with the example function `fun signDocument(documentId: UUID, code: String): Unit` and argues the reader cannot tell what could go wrong from it.
- In Kotlin, Unit means the function completes without returning a meaningful value, roughly equivalent to void in Java.
- Failures the signDocument example must reckon with include: the code might be invalid, the signing window might have closed, the database might be down, the document might already be signed or expired, or the request might have arrived out of order from a buggy client. None is visible in the signature.
- To discover possible failures and how to handle them, the post says you could open the implementation, then the service it calls, then the exception handlers, the route mapping, the tests, the OpenAPI spec, and the client code that consumes it, reading everything except the signature that should have told you.
- The post's discovery path names seven artifacts a reader must consult in place of the signature.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
JetBrains' Kotlin blog has published a piece arguing that a signature ending in `Unit` discloses nothing, built around one example: `fun signDocument(documentId: UUID, code: String): Unit` [1]. In Kotlin, `Unit` means the function completes without returning a meaningful value, roughly Java's `void` [2], so the line promises that the call may succeed and says nothing else.
The failures are not hypothetical. The post lists them: the code might be invalid, the signing window might have closed, the database might be down, the document might already be signed or expired, or the request might have arrived out of order from a buggy client [3]. None of that is visible in the declaration. To recover it, the author writes, you open the implementation, then the service it calls, then the exception handlers, the route mapping, the tests, the OpenAPI spec, and the client code [4]. That is seven artifacts read in sequence to reconstruct what one line should have stated [5].
The useful part of the argument is not the `Either` type. It is the triage that comes before it. The post splits bad outcomes into three groups [6]. API client errors, such as malformed JSON, a missing header, an unsupported operation, an out-of-sequence request, or access that is not allowed, are things a working client should almost never produce, and no designer has drawn a screen for them; they collapse into coarse HTTP responses, a 400, a 403, a 404, and are not enumerated one by one in the domain model [7]. Unexpected exceptions, such as an unavailable database, a dependency timeout, a dropped network, a `NullPointerException`, or a broken invariant, are not business outcomes at all; they become operational signals, a 500 to the client, a stack trace in the logs, a spike in the error-rate metric, a page to whoever is on call [8].
The third group is the one that has to appear in the type. The signing code was wrong, the window has closed, the document was already signed, approval is missing, the policy rejected it: cases where the client behaved correctly and the operation still cannot succeed, and where designers have a specific screen for each [9]. The test the post proposes is blunt and checkable in review: if a healthy client needs to handle two outcomes differently, those two outcomes must be distinguishable in the type [9]. That produces `fun signDocument(documentId: UUID, code: String): Either<DocumentSignError, Unit>`, with inputs on the left and the expected failure type alongside the success type on the right [10]. Two of the three categories stay out of the domain model entirely [11].
The stated rule is that a failure belonging to the business logic belongs in the signature, the API contract, and the client's handling code, not buried in the implementation [12], with the goal that the signature alone is enough to know how to call the function and how to handle every expected outcome [13]. The author works on authentication and verification at Salmon and says a mishandled failure is rarely cosmetic, because the difference between two error cases can be the difference between letting the right person through and the wrong one [14]. The accompanying material about Salmon's engineering commitments is the least load-bearing part of the argument [15].
Watch where teams draw the boundary, because that is where the work actually is: whoever decides that a timeout is operational and a closed window is domain is deciding what users see. The post says it often sees people mistakenly dragging the second category into the domain model, but the text available breaks off mid-sentence at exactly that point [16], so the guidance on the hardest case is missing. The mechanism is not Kotlin-specific; the author says the concept carries to any language with sealed types [17].