Skip to content

Instantly share code, notes, and snippets.

@erdeszt
Last active September 25, 2025 22:11
Show Gist options
  • Select an option

  • Save erdeszt/09340fc8a2fcb2bc72d6cdb1d7405725 to your computer and use it in GitHub Desktop.

Select an option

Save erdeszt/09340fc8a2fcb2bc72d6cdb1d7405725 to your computer and use it in GitHub Desktop.
Kotlin checked exceptions
package org.gl.common
sealed interface Result<out Error, out Value> {
fun <Value1> map(f: (Value) -> Value1): Result<Error, Value1> = when (this) {
is Failure -> Failure(error)
is Success -> Success(f(value))
}
fun <Error1> leftMap(f: (Error) -> Error1): Result<Error1, Value> = when (this) {
is Failure -> Failure(f(error))
is Success -> Success(value)
}
}
class Failure<out Error, out Value>(val error: Error) : Result<Error, Value>
class Success<out Error, out Value>(val value: Value) : Result<Error, Value>
class Raise<in Error : Exception> private constructor() {
companion object {
suspend fun <Error : Exception, Out> safely(
klass: Class<Error>,
body: Raise<Error>.() -> Out,
handler: (Error) -> Out
): Out {
val raiseInstance = Raise<Error>()
return with(raiseInstance) {
raiseInstance.recover(body, handler, { klass.isAssignableFrom(it.javaClass) })
}
}
}
fun raise(error: Error): Nothing {
throw error
}
operator fun invoke(error: Error): Nothing {
raise(error)
}
fun <R> liftEither(either: Result<Error, R>): R {
return when (either) {
is Failure -> throw either.error
is Success -> either.value
}
}
// TODO: Consider restricting Error0=Error
context(_: Raise<Error>)
@Suppress("UNCHECKED_CAST")
suspend fun <Error0 : Exception, Out> recover(
body: suspend Raise<Error0>.() -> Out,
mapper: (Error0) -> Out,
verifier: (Exception) -> Boolean
): Out {
try {
with(Raise<Error0>()) {
return@recover body()
}
} catch (error: Exception) {
if (verifier(error)) {
return@recover mapper(error as Error0)
} else {
throw error
}
}
}
context(raiseInstance: Raise<Error>)
suspend fun <Error0 : Exception, Out> handle(
body: suspend Raise<Error0>.() -> Out,
mapper: (Error0) -> Error,
verifier: (Exception) -> Boolean
): Out {
return recover(body, {
raiseInstance.raise(mapper(it))
}, verifier)
}
}
context(raiseInstance: Raise<Error>)
fun <Error : Exception> Error.raise(): Nothing {
raiseInstance(this)
}
context(raiseInstance: Raise<Error>)
suspend inline fun <reified Error0 : Exception, Error : Exception, T> ((Error0) -> Error).handles(noinline body: suspend Raise<Error0>.() -> T): T {
return raiseInstance.handle(body, this, { it is Error0 })
}
context(raiseInstance: Raise<Error>)
suspend inline fun <reified Error0 : Exception, Error : Exception, T> ((Error0) -> T).recovers(noinline body: suspend Raise<Error0>.() -> T): T {
return raiseInstance.recover(body, this, { it is Error0 })
}
context(raiseInstance: Raise<Error>)
fun <Error : Exception, R> Result<Error, R>.toRaise(): R {
return raiseInstance.liftEither(this)
}
// --------------- Example --------------
context(_: Raise<EmailAlreadyInUseError>)
override suspend fun register(email: Email, password: PlainPassword): User {
val user = { _: TestError -> EmailAlreadyInUseError(email) }.handles {
userService.create(email, password)
}
val user2 = { _: TestError -> EmailAlreadyInUseError(email) }.recovers {
null
}
val user3 = { _: EmailAlreadyInUseError -> EmailAlreadyInUseError(email) }.recovers {
register(email, password)
}
val euser: Result<TestError, String> = Failure(TestError())
val euser1 = euser.mapError { EmailAlreadyInUseError(email) }.toRaise()
return user
}
context(_: Raise<EmailAlreadyInUseError>, _: Raise<TestError>)
override suspend fun create(email: Email, password: PlainPassword): User {
val existingUserWithSameEmail = repo.getByEmail(email)
if (existingUserWithSameEmail != null) {
EmailAlreadyInUseError(email).raise()
}
val hashedPassword = passwordHasher.hash(password)
val user = repo.create(email, hashedPassword)
return user
}
class Raise<in T : Throwable> {
operator fun invoke(exception: T): Nothing {
throw exception
}
}
sealed class BusinessError(message: String, cause: Throwable? = null) : Exception(message, cause)
data class IncorrectValueError(val value: String) : BusinessError("Incorrect value: ${value}")
class OtherError() : BusinessError("Some other mistake")
context(raise: Raise<IncorrectValueError>)
fun blExample(value: String): Unit {
if (value == "yo") {
return
} else {
raise(IncorrectValueError("Not a good value: ${value}"))
}
}
context(raise: Raise<OtherError>)
fun blOther(): Unit {
raise(OtherError())
}
context(_: Raise<IncorrectValueError>, _: Raise<OtherError>)
fun blBoth(): Unit {
blExample("yo")
blOther()
}
context(_: Raise<BusinessError>)
fun blUpperBound(): Unit {
blExample("yo")
blOther()
}
interface X {
context(raise: Raise<BusinessError>)
fun blah(): Unit {
raise(OtherError())
}
}
fun main(args: Array<String>) {
with(Raise<BusinessError>()) {
blExample("yo")
blOther()
blBoth()
blUpperBound()
object : X {}.blah()
}
println("Hello")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment