Functional Error Handling in Kotlin: Signatures Should Tell the Truth
How to explicitly express expected failures in function signatures using Kotlin's type system
1Introduction: The Danger of Hidden Errors
A simple document signing function in Kotlin with Unit return type shows no possible errors. Yet in reality, the code might be invalid, the signing window could be closed, or the database might be unavailable. These failures are invisible in the function signature, making it hard for callers to handle them properly. A Salmon engineer working on authentication systems presents a functional solution to this problem.
2Three Categories of Failures
There are three fundamental error types: API client errors (HTTP 400, 403 etc.), unexpected exceptions (operational issues like database crashes), and domain errors (business logic failures). Only domain errors should be explicitly declared in function signatures because they're part of normal user flows. Others should be handled via global error management.
3Clarity with Either Type
The ideal solution makes function signatures explicitly declare success and failure cases. In Kotlin, a return type like Either<DocumentSignError, Unit> communicates all possible domain errors to callers. The DocumentSignError sealed class covers all domain failures like SigningWindowClosed, SignatureRejected. This approach enforces error handling at compile time.
4Shortcomings of Traditional Approach
The common 'validate then throw' pattern in Java/Kotlin ecosystems turns exceptions into hidden conditions. While function signature says 'returns nothing', implementation tells a different story. Here, compiler can't guarantee callers handle all exception types. When new exceptions are added, all call sites must be manually updated.
5Advantages of Sealed Classes
Kotlin's sealed classes allow modeling all possible error cases as a finite set. The compiler can verify when expressions exhaust all cases. This forces developers to update all usage points when adding new error types, preventing runtime-discovered failures. It brings compile-time safety to error handling.
6Domain-Driven Design
Any situation that a healthy client needs to handle differently should be explicitly modeled in the type system. 'Signing window closed' and 'wrong signature code' require different UI treatments. While HTTP 400 is a technical error, 'approval missing' is a business rule. Domain errors should be treated as part of application's core logic.
7Salmon's Engineering Culture
Salmon operates on three core engineering principles: real ownership from day one, openly held high standards, and refusal to ship things that don't actually work. Functions that hide failures contradict all three. Therefore, the team strives to make all expected failures visible in function signatures.
8Implementation Example
Improved signature example: fun signDocument(documentId: UUID, code: String): Either<DocumentSignError, Unit>. DocumentSignError is a sealed class with subclasses like SigningWindowClosed, SignatureRejected, AlreadySigned. Callers must handle all cases via when expressions. This approach also brings transparency to code reviews and API documentation.
9Fear of Functional Programming
The term 'functional error handling' scares many developers with monads and category theory. But the goal here is simple: function signature should be sufficient to know how to call it and handle every expected outcome. Nothing should be hidden in implementation. No complex math is needed - just clarity.
10Cross-Language Applicability
While the article uses Kotlin examples, the concept applies to any language with sealed types. TypeScript's union types, Swift's enums, Rust's Result type can be used similarly. The core philosophy remains: explicitly model all expected failure cases in type system and leverage compiler to enforce these checks.
11Maintainability and Extensibility
With explicit error types, systems can be extended more safely when new error cases are added. When developer adds a new DocumentSignError variant, compiler reminds to update all call sites. This is particularly valuable in large teams and long-lived projects. Compile-time checks should be preferred over runtime failures.
12Conclusion: Signatures Are Contracts
A function signature is a contract with its caller. No matter how simple, every function should explicitly declare its success and failure cases. Kotlin features like sealed classes and Either type make these contracts compiler-enforced. In Salmon's financial systems, this approach is critical for security and reliability. True ownership is only possible with visible failures.