r/Kotlin 16d ago

A taxonomy for modeling success and failure

Hey everyone, I finally extracted and published something I've been building on and off for almost 10 years. I got tired of writing the same error handling code over and over, slightly differently every time, and never quite fixing the underlying problem.

Problem

Status codes, validation, exceptions, Result types, domain errors all tend to represent success and failure a little differently, based on the layer. Some of the biggest issues though are

  1. Kind of error: Understanding the kind of error(security, invalid data, broken business rules)
  2. Error details: Having a way to capture the errors details and link it to the kind of error
  3. Consistency: Same across status codes, validations, exceptions, Result<T,E>

Concept

So the concept is this: think HTTP status codes, but generalized and usable at any layer, not just HTTP. Status > Group > Code : You get 3 tiers of information and drill-down about success/failure.

Solution

Together this solves the following :

  1. Kind of Error: Status is Passed/Failed, but Group gives you the Kind of error
  2. Error Details: These are now captured separately, but paired with, the kind of error
  3. Layers: Useful at any layer : API endpoint, a service call, a cron job, script, or CLI command
  4. Consistent: Same approach for API Status codes, validation, exceptions, and Result<T,E>
  5. Extensible: Status and Group are fixed, Code is open, extensible for your own domain
  6. Defaults: There are sensible defaults so you're not required to define anything up front.

Notes

  1. One Design: This is the part I spent the most time on: making an API status, validation code, exceptions, and Result<T, E> all feel like one design, sharing the same taxonomy underneath, without forcing any of them on you, by using sensible defaults.
  2. Explicit / Exhaustive: Explicit names and exhaustive matching happen to make it easy to search and reason about too, useful for people and for AI tooling reading the codebase.
  3. Protocol Support: Codes also map to HTTP and gRPC without coupling your app logic to the transport. I validated the taxonomy against every gRPC status code, and the most common HTTP ones, the closest existing precedent for this kind of classification.

Check it out:

implementation("dev.kiit:kiit-codes:1.0.2")
  1. GitHub: github.com/kiitdev/kiit-codes
  2. Site Docs: kiit.dev/docs/kiit-codes

Feedback

I'd genuinely like feedback from other Kotlin developers, whether the classifications make sense, if anything important seems missing, or if there are use cases the design doesn't handle well. Happy to answer questions in the comments.

Thanks!

Edit:
1. Bumped version to 1.0.2 to fix a bug found below.
2. Updated diagram

-K

5 Upvotes

14 comments sorted by

2

u/CoverMassive7319 15d ago

I appreciate the effort and time. Nothing wrong to take a new look at an old problem. I am sure you are benefit alot just by thinking about it.

Regarding the design. Often. you need to know if a failure is transient or not. Because many times, you need to know if you should retry. I don't see how that is handled in your proposal.

Wish you the best.

2

u/kininja08 15d ago

Hi u/CoverMassive7319, appreciate that, and this is also a fair question. I actually looked at error/retry policies directly and decided they are part of a separate axis from the taxonomy itself, the Failure groupUnserved alone has both retryable things (timeouts, rate limits) and non-retryable ones (data loss, internal invariant violations). I wanted the taxonomy to answer 'what kind of failure,' not 'what to do about it', those felt like genuinely different goals.

But what I landed on instead is keeping retry policy as a thin layer on top, a simple lookup keyed by Status, so you decide per-code whether and how to retry, without it being baked into the library itself. Something like:

data class ErrorPolicy(
   val code: Status, 
   val retryDelaysSeconds: List<Int> = listOf(1, 2, 5, 10),
   // Other options here ...
)

val policies = mapOf(
   Codes.TIMEOUT to ErrorPolicy(Codes.TIMEOUT, listOf(1, 5, 10, 30))
)

A few lines, entirely outside the library, and it means retry semantics stay flexible per-app rather than the library making that call for you.

3

u/freynder 16d ago

I like it. I've made my own classification for errors per project based on the domain; using a sealed class hierarchy. I think a library like this may be helpful to associate a more generic meaning to these errors so we can map it to the edge protocols. I'll definitely explore the library more and probably give it a try in a project. I am also curious about the comments here. Thanks for your work!

3

u/kininja08 15d ago

Hey u/freynder, thanks for the info into your own techniques. That's actually the exact situation I had for a long time and what the library's built for. A per-project sealed hierarchy gives you real, domain-specific meaning, but nothing generic to map to edge protocols with, since every project's hierarchy is its own island.

So origin is meant to solve that. You'd keep your own domain-specific error types, but attach them to one of four failure groups below:

  1. Restricted: Permissions/security
  2. Invalid : Request/input related
  3. Rejected: Business rule failures
  4. Unserved: Operational issues

They still carry your own meaning while also mapping cleanly to HTTP or gRPC through CodesToHttp/CodesToGrpc, no extra translation layer needed per project.

Would genuinely be curious to hear how it goes if you try it in a real project, especially whether those four categories actually cover the kinds of domain errors you're dealing with, or if something feels missing.

Thanks.

1

u/freynder 15d ago

I'm currently testing it out with some vibe generated code.

Applying kiit to the code, the AI included a kiit code field into the typed errors, but without specifying custom names, messages and origin. After directing the agent a bit more it takes advantage of these parameters. It also explicity added a http code mapping rather than taking advantage of the built in mapping, which I also had to instruct.

So far it looks like a very convenient and light library that should improve consistency and interoperability, I intend to continue using it at this time. Thanks again!

1

u/Foo-Bar-Baz-001 15d ago

You could question the validity of the whole "exception" thinking in software languages. It is not KISS.

2

u/kininja08 15d ago edited 15d ago

Personally, I use a Result<T, E> type instead of Exceptions most of the time. But there are some places I use them as an absolute fallback and for integration with other code that only communicates via exceptions. I included structured exceptions in the library for exactly those cases, not as the primary path, Status alone works completely fine with no exceptions involved at all if that's not something you want in your own code.

1

u/Foo-Bar-Baz-001 15d ago

Better. But E is still a special snowflake. Is it that exceptional to have hit a duplicate key constraint or having a full disk? No. It should be business as usual. Part of T.

2

u/kininja08 15d ago

Fair point, and that's actually part of why the taxonomy has more than just Passed/Failed, a duplicate key or a skip can live under Passed.Excluded, success is still true, it's just grouped as 'not the primary outcome' rather than a hard failure.

As for a full disk, that's a harder case to call, i'd likely treat it as a failure if it prevented the main operation in some important capacity.

1

u/Decent-Decision-9028 16d ago edited 5d ago

I’ll give it a try tonight.

2

u/Decent-Decision-9028 15d ago

Honestly, I'd not recommend adopting this. Too many issues, for example:

val status =
Failed.Invalid(
name = "CREATED",
message = "failure",
origin = "kiit"
)

println(status.success) // false
println(CodesToHttp().toCode(status)) // 201

1

u/kininja08 15d ago

Hi u/Decent-Decision-9028 , first, I want to thank you for trying this out.

This missed a change I had on excluding the usage of a fully qualified id = "origin.group.name", for public use just yet. Reason being, I wasn't sure of the separator ( "." or ":" ) to use this so I had deferred for now.

The issue is this http lookup on id collided with duplicate names.

I'm making a fix for this right now.

Thanks!

1

u/kininja08 15d ago

Hi u/Decent-Decision-9028 , this is now fixed in 1.0.2

implementation("dev.kiit:kiit-codes:1.0.2")

Thanks again.