kiit-codes
A Kotlin library for classifying and handling success and failure.
A small, dependency-free status and error taxonomy for application outcomes, with
extensible codes, protocol mappings, validation, typed exceptions, and optional
Result<T, E> integration.

Overview
Goals
Applications need to communicate a simple idea consistently: what happened? In practice, success and failure get modeled differently across domains, layers, and protocols, which causes recurring problems: no shared taxonomy for classifying outcomes, inconsistent handling across layers, validation/exceptions/statuses/results all using different approaches, similar error types rebuilt project to project, and generic errors that lose domain-specific meaning.
kiit-codes exists to provide a shared, application-level model for these concerns: a fixed taxonomy for consistent classification, extensible codes that preserve domain-specific meaning, and protocol mappings that keep application outcomes independent from how they're transported. The same model is then reused across statuses, validation, exceptions, and result types.
Inspiration
| # | Source | What was drawn from it |
|---|---|---|
| 1 | HTTP status codes | Validated against, not derived from — the most common HTTP codes map onto kiit-codes' eight groups without needing a ninth. |
| 2 | gRPC status codes | Same validation as HTTP — every gRPC code maps onto the existing eight groups. |
| 3 | Scala's Either/Try | Built on Either's two-branch shape, structured around Try's branches, with the Status taxonomy incorporated on top. |
Activity
The core classification model has years of internal production use inside the original Kiit framework, powering both mobile and server Kotlin applications, prior to being extracted into this standalone repository. The public package version reflects the standalone repo's youth, not the underlying design's: the classification itself is settled, while newer additions (JS/TS export, iOS/Swift export via SKIE) have less track record and are still being exercised.
Resources
| # | Resource | Details |
|---|---|---|
| 1 | Repository | github.com/kiitdev/kiit-codes |
| 2 | Maven coordinate | dev.kiit:kiit-codes |
| 3 | npm coordinate | @kiit/codes (JS/TS export, not CI-gated yet) |
| 4 | Related module | kiit-result builds a Result<T, E> type on top of this same taxonomy (docs page coming next) |
| 5 | API reference | Generated from source KDoc, linked here once published |
Prefer to see it work first? Jump to the Tutorial. Prefer the reasoning first? Keep reading.
Setup
Install
dependencies {
implementation("dev.kiit:kiit-codes:1.0.1")
}
Source
| # | Item | Link |
|---|---|---|
| 1 | Git Repo | github.com/kiitdev/kiit-codes |
| 2 | Root folder of sources in repo | kiit-codes/src/commonMain/kotlin |
| 3 | Sample app | samples/sample-kotlin |
| 4 | Package Name | kiit.codes |
| 5 | Unit Tests | kiit-codes/src/commonTest |
Licensed Apache 2.0.
Example
import kiit.codes.*
fun authorize(userId: String, requesterId: String): Status =
if (userId != requesterId) Restricted.UNAUTHORIZED
else Succeeded.SUCCESS
when (val status = authorize(userId, requesterId)) {
is Passed -> log.info("ok: ${status.name}")
is Failed -> log.warn("failed: ${status.name} — ${status.message}")
}
Concepts
Terms
| # | Term | Definition | |
|---|---|---|---|
| 1 | Taxonomy | The overall Status → Group → Code classification system. | More |
| 2 | Status | Sealed interface for an operation's outcome: Passed or Failed. | More |
| 3 | Group | Second tier: a fixed subtype of Passed/Failed (e.g. Restricted). | More |
| 4 | Code | Third tier: an open Status instance within a group (e.g. DENIED). | More |
| 5 | Err | Error representation for use with Result/Outcome-style types. | More |
| 6 | Checked | Non-monadic validation result reporting every problem, not just the first. | More |
| 7 | StatusException | Sealed exception hierarchy carrying a Checked, for exception-only boundaries. | More |
Status
Every Status belongs to exactly one Group, and every concrete status value is a
Code within that Group. Passed.Succeeded.SUCCESS is the SUCCESS Code inside
the Succeeded Group, under the Passed Status. Failed.Restricted.DENIED is
the DENIED Code inside the Restricted Group, under the Failed Status.
Succeeded.CREATED is one built-in Code. Its fields, each read on its own line:
val status: Status = Succeeded.CREATED
status.name // "CREATED"
status.origin // "kiit"
status.id // "kiit.CREATED"
status.message // "A new resource was created."
status.success // true
status.group // "Succeeded"
| Field | Definition |
|---|---|
| name | Stable SCREAMING_SNAKE_CASE label, e.g. "TOKEN_EXPIRED", for logs. |
| origin | Where a status came from: "kiit" for built-ins, "custom" by default. |
| id | "$origin.$name": unique across every Status, usable as a map key. |
| message | Human-readable constant description. Never built from runtime data. |
| success | true for Passed, false for Failed. |
Taxonomy
The full Status → Group → Code taxonomy: every built-in Passed and Failed group,
and every built-in code within each.

| Tier | Parent | Fixed/Open | Children | Description |
|---|---|---|---|---|
| 1 | Status | Fixed | Passed | |
| Failed | ||||
| 2 | Group | Fixed | Succeeded | The operation completed successfully. |
| Pending | The operation was accepted but has not yet fully resolved. | |||
| Excluded | The item was intentionally excluded from the operation. | |||
| Information | The response provides information; no operation was performed. | |||
| Restricted | The caller is not allowed. | |||
| Invalid | The request itself is wrong. | |||
| Rejected | The caller was allowed, but the business refuses it. | |||
| Unserved | The system can't serve it right now, though nothing was wrong with the request. | |||
| 3 | Code | Open + Defaults | Ships with common built-in codes (e.g. SUCCESS, DENIED); extensible with custom, domain-specific codes within the same group. |
Passed
Passed.success == true.
| Group | Code | Description |
|---|---|---|
| Succeeded | SUCCESS | The operation completed successfully. |
| CREATED | A new resource was created. | |
| UPDATED | The resource was fully updated. | |
| PATCHED | The resource was partially updated. | |
| FETCHED | The resource was retrieved. | |
| DELETED | The resource was deleted. | |
| HANDLED | The request was handled; nothing to return. | |
| REFERRED | The result is at another location. | |
| EXITED | The application exited cleanly. | |
| Pending | ACCEPTED | The request was accepted. |
| QUEUED | The request is waiting to be processed. | |
| PROCESSING | The request is being processed. | |
| CONFIRM | The request is awaiting confirmation. | |
| REDIRECTED | This request is being handled elsewhere. | |
| SCHEDULED | The operation is scheduled for later. | |
| Excluded | OMITTED | The item was excluded from the result. |
| SKIPPED | The item was not processed. | |
| DISCARDED | The item was processed, then excluded for unrelated reasons. | |
| CANCELLED | The operation was cancelled by the caller before completion. | |
| DEDUPLICATED | The duplicate item was not processed. | |
| DISQUALIFIED | The item was disqualified. | |
| Information | NOTICE | An informational notice. |
| ADVISORY | A notice that may need attention. | |
| METADATA | Information about the application itself was returned. | |
| HEALTH | The service is healthy and operational. | |
| DIAGNOSTICS | Diagnostic or operational information was returned. | |
| MOVED | The resource has permanently moved to a new location. |
Failed
Failed.success == false.
| Group | Code | Description |
|---|---|---|
| Restricted | DENIED | The request was denied. |
| UNAUTHENTICATED | Authentication is required. | |
| UNAUTHORIZED | The caller lacks permission. | |
| FORBIDDEN | Access to this resource is forbidden. | |
| LOCKED | Access is locked; resolve the condition to restore access. | |
| SUSPENDED | Access has been administratively suspended. | |
| Invalid | INVALID_VALUE | The request had an invalid value. |
| BAD_REQUEST | The request was malformed. | |
| NOT_FOUND | The requested route or endpoint does not exist. | |
| OUT_OF_RANGE | A value was outside the acceptable range. | |
| PAYLOAD_TOO_LARGE | The payload is too large. | |
| MISSING_FIELD | A required field was not provided. | |
| Rejected | RULE_VIOLATION | A business rule rejected the request. |
| CONFLICT | The request conflicts with the current state. | |
| NOT_EXISTS | The referenced item does not exist. | |
| PRECONDITION_FAILED | A required precondition was not met. | |
| EXPIRED | The item has expired. | |
| GONE | The resource was removed and is no longer available. | |
| Unserved | UNEXPECTED | An unexpected, unclassified error occurred. |
| UNSUPPORTED | This capability is not currently available. | |
| TIMEOUT | The operation timed out. | |
| RATE_LIMITED | Too many requests; try again later. | |
| RESOURCE_LIMITED | A resource limit has been reached. | |
| UNREACHABLE | A required dependency could not be reached. | |
| UNDER_MAINTENANCE | The service is temporarily under maintenance. | |
| INTERNAL | An internal invariant was violated. | |
| DATA_LOSS | Unrecoverable data loss or corruption occurred. | |
| DEGRADED | This dependency is degraded; some calls may be refused. | |
| LEGAL_BLOCK | Access is blocked for legal reasons. | |
| ABORTED | The operation was aborted; retrying may help. |
Err
| Variant | Fields | Use |
|---|---|---|
Err.ErrorInfo | message, cause?, ref? | Default implementation: a message with an optional cause. |
Err.ErrorField | field, value, message, cause?, ref? | An error on a specific field. |
Err.ErrorList | errors, message, cause?, ref? | Wraps a list of other errors. |
Builders: Err.of(message), Err.of(status), Err.on(field, value, message),
Err.on(field, message) (value omitted for sensitive fields), Err.ex(throwable),
Err.obj(any), Err.list(strings, message), Err.build(any?).
Checked
Checked(status: Status, errors: List<Err>), constructed only through Checked.success(status)
or Checked.failure(status, errors), so status and errors can never disagree: a passing
Checked always has an empty errors list, a failing one always has at least one entry.
isValid: Boolean reflects errors.isEmpty(). Implements HasErrors. collect(vararg checks) /
collect(checks: List<Checked>) combine multiple Checked into one, failing with
Invalid.INVALID_VALUE and every pooled error if any input failed.
Exceptions
Sealed, with four subclasses matching the Failed groups: RestrictedException,
InvalidException, RejectedException, UnservedException. Each carries a Checked, exposed
as status: Status and errors: List<Err>. Failed.toException(errors) converts a bare
Failed status into the matching subclass. Platform-idiomatic equivalents exist for iOS
(@ObjCName in iosMain) and JS/TS (jsMain).
Protocols
| Type | Purpose |
|---|---|
CodesToHttp | Maps Status to/from HTTP status codes. |
CodesToGrpc | Maps Status to/from gRPC status codes. |
CodeLookup | Interface for defining a mapping to any other protocol. |
CompositeLookup | Combines a base CodeLookup with per-code extensions/overrides. |

Design
Philosophy
A closed taxonomy keeps generic handling, exhaustive matching, logging, and protocol mappings
consistent everywhere a status is used. Codes stay open underneath so each domain can extend the
taxonomy freely without forking it. This doesn't replace domain modeling: domain errors explain
what happened in one domain, the taxonomy explains what kind of outcome it was, consistently,
across every domain in an application. Status is a sealed interface rather than an enum
specifically so consumers can add their own codes while still participating in the same
taxonomy — an enum can't be extended this way.
Features
| Feature | Description |
|---|---|
| Status classification | The core Passed/Failed taxonomy. |
| Extensibility | Domain-specific codes within the same fixed groups. |
| Protocol mappings | HTTP, gRPC, and custom protocol lookups. |
| Validation | Checked/Err/collect for reporting every problem found. |
| Typed exceptions | StatusException for exception-only boundaries. |
| Result integration | kiit-result's Result<T, E> built on this taxonomy. |
Limitations
| # | Limitation | Details |
|---|---|---|
| 1 | Single maintainer | Apache 2.0 licensed and source available, but no second maintainer or organizational backing yet. |
| 2 | AI framing is unproven | Stable names and explicit classification are expected to reduce ambiguity for AI tooling, but that's a hypothesis, not a benchmarked result. |
| 3 | JS/TS not CI-gated | Exists but isn't CI-gated or published to npm yet; lacks the compiler-enforced exhaustiveness that Kotlin, Java, and Swift (via SKIE) get. |
Exclusions
| # | Excluded | Reasoning |
|---|---|---|
| 1 | Retry logic or severity levels | Retryability cuts across groups rather than aligning with them — Unserved alone has both retryable and non-retryable codes. A dedicated Retry category was considered and rejected for the same reason. |
| 2 | A numeric status code field | An earlier version had one; it invited the wrong inference (looking like an HTTP code while meaning something else). Real protocol numbers are available on demand via CodesToHttp/CodesToGrpc, never implied by the taxonomy itself. |
| 3 | A ninth group | Every gRPC code and the most common HTTP codes map onto the existing eight without needing one, tested directly against both. |
Tutorial
Code
This walks through building a tiny service that returns Status for expected outcomes, then
crosses a boundary that can only communicate via exceptions.
Define a service that returns a Status instead of throwing for expected failures:
import kiit.codes.*
data class User(val id: String, val email: String)
class UserService {
private val users = mutableMapOf<String, User>()
fun create(id: String, email: String): Status {
if (email.isBlank()) return Invalid.BAD_REQUEST
if (users.containsKey(id)) return Rejected.CONFLICT
users[id] = User(id, email)
return Succeeded.CREATED
}
fun authorize(id: String, requesterId: String): Status =
when {
!users.containsKey(id) -> Rejected.NOT_EXISTS
id != requesterId -> Restricted.UNAUTHORIZED
else -> Succeeded.SUCCESS
}
}
Callers
Call it and branch on the result:
val service = UserService()
val created = service.create("alice", "alice@example.com")
println("${created.name} (success=${created.success})") // CREATED (success=true)
val denied = service.authorize("alice", "bob")
println("${denied.name} (success=${denied.success})") // UNAUTHORIZED (success=false)
Try/Catch
Now add a method that throws instead, for a caller that only understands exceptions:
fun UserService.requireAuthorized(id: String, requesterId: String) {
val status = authorize(id, requesterId)
if (status is Failed) throw status.toException()
}
try {
service.requireAuthorized("alice", "bob")
} catch (e: StatusException) {
println("caught: ${e.status.name} — ${e.message}")
// caught: UNAUTHORIZED — Not authorized to perform this action
}
status.toException() picked StatusException.RestrictedException automatically, since
Restricted.UNAUTHORIZED belongs to the Restricted group. See Concepts
for the full exception hierarchy, or Design for why the taxonomy is shaped this
way.
Guide
Usage
Status only, when the outcome itself is enough:
when (val status = authorize(userId, requesterId)) {
is Passed -> log.info("ok: ${status.name}")
is Failed -> log.warn("failed: ${status.name} — ${status.message}")
}
Extensibility — custom codes stay inside a built-in group:
val PAYMENT_DECLINED = Failed.Rejected(
name = "PAYMENT_DECLINED",
message = "Payment declined",
origin = "payments",
)
PAYMENT_DECLINED remains a Rejected outcome everywhere in the system while retaining its own
domain-specific identity. origin keeps custom namespaces distinct from "kiit" and from other
teams' codes.

Validation, reporting every problem instead of stopping at the first:
fun validateUser(name: String, email: String): Checked {
val errors = mutableListOf<Err>()
if (name.isBlank()) errors.add(Err.on("name", name, "Name is required"))
if (!email.contains("@")) errors.add(Err.on("email", email, "Email must contain @"))
return if (errors.isEmpty()) Checked.success() else Checked.failure(Invalid.INVALID_VALUE, errors)
}
Exceptions, converting a Failed status at a boundary that needs one:
fun requireAuthorized(id: String, requesterId: String) {
val status = authorize(id, requesterId)
if (status is Failed) throw status.toException()
}

Protocols
HTTP, via CodesToHttp:
val http = CodesToHttp()
http.toCode(Succeeded.CREATED) // 201
http.toCode(Invalid.INVALID_VALUE) // 400
http.toStatus(404)?.name // "NOT_FOUND"
gRPC, via CodesToGrpc:
val grpc = CodesToGrpc()
grpc.toCode(Restricted.DENIED) // 7, PERMISSION_DENIED
grpc.toStatus(6)?.name // "CONFLICT", ALREADY_EXISTS reversed
Custom protocols, via CodeLookup/CompositeLookup:
val lookup = CompositeLookup(
base = CodesToHttp(),
extensions = mapOf(PAYMENT_DECLINED to 402),
)
lookup.toCode(PAYMENT_DECLINED) // 402