Skip to main content

kiit-codes logokiit-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.

Kiit Codes overview

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

#SourceWhat was drawn from it
1HTTP status codesValidated against, not derived from — the most common HTTP codes map onto kiit-codes' eight groups without needing a ninth.
2gRPC status codesSame validation as HTTP — every gRPC code maps onto the existing eight groups.
3Scala's Either/TryBuilt 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

#ResourceDetails
1Repositorygithub.com/kiitdev/kiit-codes
2Maven coordinatedev.kiit:kiit-codes
3npm coordinate@kiit/codes (JS/TS export, not CI-gated yet)
4Related modulekiit-result builds a Result<T, E> type on top of this same taxonomy (docs page coming next)
5API referenceGenerated 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

#ItemLink
1Git Repogithub.com/kiitdev/kiit-codes
2Root folder of sources in repokiit-codes/src/commonMain/kotlin
3Sample appsamples/sample-kotlin
4Package Namekiit.codes
5Unit Testskiit-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

#TermDefinition
1TaxonomyThe overall Status → Group → Code classification system.More
2StatusSealed interface for an operation's outcome: Passed or Failed.More
3GroupSecond tier: a fixed subtype of Passed/Failed (e.g. Restricted).More
4CodeThird tier: an open Status instance within a group (e.g. DENIED).More
5ErrError representation for use with Result/Outcome-style types.More
6CheckedNon-monadic validation result reporting every problem, not just the first.More
7StatusExceptionSealed 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"
FieldDefinition
nameStable SCREAMING_SNAKE_CASE label, e.g. "TOKEN_EXPIRED", for logs.
originWhere a status came from: "kiit" for built-ins, "custom" by default.
id"$origin.$name": unique across every Status, usable as a map key.
messageHuman-readable constant description. Never built from runtime data.
successtrue 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.

Kiit Codes taxonomy

TierParentFixed/OpenChildrenDescription
1Status FixedPassed
Failed
2Group FixedSucceededThe operation completed successfully.
PendingThe operation was accepted but has not yet fully resolved.
ExcludedThe item was intentionally excluded from the operation.
InformationThe response provides information; no operation was performed.
RestrictedThe caller is not allowed.
InvalidThe request itself is wrong.
RejectedThe caller was allowed, but the business refuses it.
UnservedThe system can't serve it right now, though nothing was wrong with the request.
3Code Open + DefaultsShips with common built-in codes (e.g. SUCCESS, DENIED); extensible with custom, domain-specific codes within the same group.

Passed

Passed.success == true.

GroupCodeDescription
SucceededSUCCESSThe operation completed successfully.
CREATEDA new resource was created.
UPDATEDThe resource was fully updated.
PATCHEDThe resource was partially updated.
FETCHEDThe resource was retrieved.
DELETEDThe resource was deleted.
HANDLEDThe request was handled; nothing to return.
REFERREDThe result is at another location.
EXITEDThe application exited cleanly.
PendingACCEPTEDThe request was accepted.
QUEUEDThe request is waiting to be processed.
PROCESSINGThe request is being processed.
CONFIRMThe request is awaiting confirmation.
REDIRECTEDThis request is being handled elsewhere.
SCHEDULEDThe operation is scheduled for later.
ExcludedOMITTEDThe item was excluded from the result.
SKIPPEDThe item was not processed.
DISCARDEDThe item was processed, then excluded for unrelated reasons.
CANCELLEDThe operation was cancelled by the caller before completion.
DEDUPLICATEDThe duplicate item was not processed.
DISQUALIFIEDThe item was disqualified.
InformationNOTICEAn informational notice.
ADVISORYA notice that may need attention.
METADATAInformation about the application itself was returned.
HEALTHThe service is healthy and operational.
DIAGNOSTICSDiagnostic or operational information was returned.
MOVEDThe resource has permanently moved to a new location.

Failed

Failed.success == false.

GroupCodeDescription
RestrictedDENIEDThe request was denied.
UNAUTHENTICATEDAuthentication is required.
UNAUTHORIZEDThe caller lacks permission.
FORBIDDENAccess to this resource is forbidden.
LOCKEDAccess is locked; resolve the condition to restore access.
SUSPENDEDAccess has been administratively suspended.
InvalidINVALID_VALUEThe request had an invalid value.
BAD_REQUESTThe request was malformed.
NOT_FOUNDThe requested route or endpoint does not exist.
OUT_OF_RANGEA value was outside the acceptable range.
PAYLOAD_TOO_LARGEThe payload is too large.
MISSING_FIELDA required field was not provided.
RejectedRULE_VIOLATIONA business rule rejected the request.
CONFLICTThe request conflicts with the current state.
NOT_EXISTSThe referenced item does not exist.
PRECONDITION_FAILEDA required precondition was not met.
EXPIREDThe item has expired.
GONEThe resource was removed and is no longer available.
UnservedUNEXPECTEDAn unexpected, unclassified error occurred.
UNSUPPORTEDThis capability is not currently available.
TIMEOUTThe operation timed out.
RATE_LIMITEDToo many requests; try again later.
RESOURCE_LIMITEDA resource limit has been reached.
UNREACHABLEA required dependency could not be reached.
UNDER_MAINTENANCEThe service is temporarily under maintenance.
INTERNALAn internal invariant was violated.
DATA_LOSSUnrecoverable data loss or corruption occurred.
DEGRADEDThis dependency is degraded; some calls may be refused.
LEGAL_BLOCKAccess is blocked for legal reasons.
ABORTEDThe operation was aborted; retrying may help.

Err

VariantFieldsUse
Err.ErrorInfomessage, cause?, ref?Default implementation: a message with an optional cause.
Err.ErrorFieldfield, value, message, cause?, ref?An error on a specific field.
Err.ErrorListerrors, 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

TypePurpose
CodesToHttpMaps Status to/from HTTP status codes.
CodesToGrpcMaps Status to/from gRPC status codes.
CodeLookupInterface for defining a mapping to any other protocol.
CompositeLookupCombines a base CodeLookup with per-code extensions/overrides.

Kiit Codes protocol mappings

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

FeatureDescription
Status classificationThe core Passed/Failed taxonomy.
ExtensibilityDomain-specific codes within the same fixed groups.
Protocol mappingsHTTP, gRPC, and custom protocol lookups.
ValidationChecked/Err/collect for reporting every problem found.
Typed exceptionsStatusException for exception-only boundaries.
Result integrationkiit-result's Result<T, E> built on this taxonomy.

Limitations

#LimitationDetails
1Single maintainerApache 2.0 licensed and source available, but no second maintainer or organizational backing yet.
2AI framing is unprovenStable names and explicit classification are expected to reduce ambiguity for AI tooling, but that's a hypothesis, not a benchmarked result.
3JS/TS not CI-gatedExists but isn't CI-gated or published to npm yet; lacks the compiler-enforced exhaustiveness that Kotlin, Java, and Swift (via SKIE) get.

Exclusions

#ExcludedReasoning
1Retry logic or severity levelsRetryability 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.
2A numeric status code fieldAn 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.
3A ninth groupEvery 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.

Kiit Codes custom 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()
}

Kiit Codes usage

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