EitherNet: modelling Retrofit responses as a sealed result instead of exceptions
A multiplatform, pluggable, and sealed API result type for modeling network API responses.
At a glance
- What is it?
- EitherNet wraps a Retrofit call in a sealed ApiResult type with Success and Failure branches, and Failure itself splits into network, HTTP, API and unknown errors so the compiler forces you to handle each. It ships with a test controller that fakes endpoints the way MockWebServer does, and the Retrofit implementation is the only one that exists today.
- Who is it for?
- Adopt EitherNet if you are a Kotlin team on Retrofit that keeps losing error handling in exception-driven call sites, and you want the compiler to make you handle the failure kinds explicitly. Do not adopt it if you are not on the JVM, because the README says the Retrofit implementation is the only one that exists even though the core API is defined in common code.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 143 days ago.
- What is it written in?
- Mainly Kotlin, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One return type instead of a try and catch
By default Retrofit uses exceptions to propagate any errors. EitherNet's premise is that this library leverages Kotlin sealed types to better model these responses with a type-safe single point of return and no exception handling needed. The core type is ApiResult<out T, out E>, where T is the success type and E is a possible error type, and it has two sealed subtypes, Success and Failure. Success is typed to T with no error type, and Failure is typed to E with no success type. Failure is in turn represented by four sealed subtypes of its own, Failure.NetworkFailure, Failure.ApiFailure, Failure.HttpFailure and Failure.UnknownFailure. The result is that a call site has one exit, and the compiler checks that you handled the branch you meant to handle, rather than leaving a catch block to be written and forgotten.
Sealed branches, not stringly typed error codes
The handling pattern the README shows is a nested when, first on Success and Failure, then on the specific failure subtype:
when (val result = myApi.someEndpoint()) {
is Success -> doSomethingWith(result.response)
is Failure -> when (result) {
is NetworkFailure -> showError(result.error)
is HttpFailure -> showError(result.code)
is ApiFailure -> showError(result.error)
is UnknownFailure -> showError(result.error)
}
}The README notes that user code for this could just simply show a generic error message for a Failure case, but that the sealed subtypes also allow for more specific error messaging or pluggability of error types. That is the design argument. The four failure kinds are not cosmetic, because a network failure, an HTTP error status, a decoded API error body and an unclassified failure all need different handling in a real application, and collapsing them into one exception type loses the distinction. The UnknownFailure branch is the honest admission that something happened that the library did not anticipate, and having it as a named case is better than a generic catch.
Wiring it in is two factory calls
Simply change your endpoint return type to the typed ApiResult and include the call adapter and delegating converter factory. The Retrofit builder gains two entries:
val api = Retrofit.Builder()
.addConverterFactory(ApiResultConverterFactory)
.addCallAdapterFactory(ApiResultCallAdapterFactory)
.build()
.create<TestApi>()The interface changes in one line, the return type of the suspend function:
interface TestApi {
@GET("/")
suspend fun getData(): ApiResult<SuccessResponse, ErrorResponse>
}If you do not have custom error return types, simply use Unit for the error type. That detail matters for adoption, because a codebase that has no typed error model can still convert its endpoints to ApiResult with Unit on the error side and get the branch handling, without first inventing a set of error classes for every endpoint.
Decoding error bodies when the status code is not enough
By default an error response does not give you a typed body. If you want to decode error types in HttpFailures, annotate your endpoint with @DecodeErrorBody, and a 4xx or 5xx response will try to decode its error body, if any, as ErrorResponse. If you want to contextually decode the error body based on the status code, you can retrieve a @StatusCode annotation from annotations in a custom Retrofit Converter, which is what the example converter does, mapping a 401 to Unauthorized::class.java and a 404 to NotFound::class.java before delegating to the next converter. One detail to keep in mind: error bodies with a content length of 0 will be skipped, so a bare status code with no body will not produce a decoded error instance and will land in the unclassified path instead.
Plugability covers the polymorphic 200 response
The pluggability section addresses a real API pattern. Some APIs return a polymorphic 200 response where the data needs to be dynamically parsed, with an ok flag and a data object on success, and the same structure with an error_message in an error event. That is hard to model with a single concrete type, but easy to handle with ApiResult. The instruction is to throw an ApiException with the decoded error type in a custom Retrofit Converter, and it will be automatically surfaced as a Failure.ApiFailure type with that error instance. The converter reads the error type from a @ResultType annotation through the annotations, builds a ResponseBodyConverter for it, and inside convert it checks whether the payload is an error type, throwing ApiException in that case and returning the success value otherwise. So the escape hatch for an awkward API is to write a converter, not to change the result type.
Retries with backoff are in the box
A common pattern in making network requests is to retry with exponential backoff, and EitherNet ships with a highly configurable retryWithExponentialBackoff() function for this case. The parameters are the ones you would expect, with defaults given for reference: maxAttempts of 3, an initialDelay of 500 milliseconds, a delayFactor of 2.0, a maxDelay of 10 seconds, a jitterFactor of 0.25, and an optional onFailure callback used for logging. The jitter factor is the detail worth noting, because a backoff without jitter is how a fleet of clients synchronises onto the same retry schedule and reproduces the outage it is trying to survive. The function takes the request as a trailing lambda, so api.getData() sits inside the call and the retry policy wraps it.
A test controller that behaves like MockWebServer
EitherNet ships with a Test Fixtures artifact containing an EitherNetController API to allow for easy testing with EitherNet APIs, and the README compares it to OkHttp's MockWebServer, where results can be enqueued for specific endpoints. You create a controller in a test with one of the newEitherNetController() functions, which is reified, so the type is inferred, and then you access the underlying faked api property from it and pass that on to whatever is being tested. Results are enqueued per endpoint, and you can optionally pass in full suspend functions if you need dynamic behaviour, where the block is a suspend function that can delay before returning an ApiResult. In instrumentation tests with dependency injection, you can provide the controller and its underlying API in a test module and replace the standard one, which the README says works particularly well with Anvil.
Editorial conclusion
Adopt EitherNet if you are a Kotlin team on Retrofit that keeps losing error handling in exception-driven call sites, and you want the compiler to make you handle the failure kinds explicitly. Do not adopt it if you are not on the JVM, because the README says the Retrofit implementation is the only one that exists even though the core API is defined in common code. Before you commit, look at the four failure subtypes and check that your API's error shapes fit them, and plan for the fact that the newest published release is 2.0.0 from December 2024 while the last push was on 2026-06-02.
Frequently asked questions
What is EitherNet?
It is a multiplatform, pluggable and sealed API result type for modelling network API responses, currently implemented for Retrofit on the JVM. The core type is ApiResult<out T, out E> with Success and Failure subtypes, and Failure itself has NetworkFailure, ApiFailure, HttpFailure and UnknownFailure subtypes.
How do I set up EitherNet with Retrofit?
Change your endpoint return type to ApiResult and add two factories to the Retrofit builder, ApiResultConverterFactory and ApiResultCallAdapterFactory. If you have no custom error return types, simply use Unit for the error type.
How do I decode an error body in EitherNet?
Annotate the endpoint with @DecodeErrorBody, and a 4xx or 5xx response will try to decode its error body as the error type. To decode differently per status code, read the @StatusCode annotation from the annotations in a custom Retrofit Converter. Error bodies with a content length of 0 are skipped.
How do I test code that uses EitherNet?
EitherNet ships a Test Fixtures artifact with an EitherNetController, similar to OkHttp's MockWebServer. You create one with newEitherNetController<PandaApi>() and enqueue results per endpoint, optionally with a suspend function for dynamic behaviour, and in instrumentation tests with DI you can provide the controller in a test module, which works particularly well with Anvil.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/slackhq-eithernet)Was this useful?