DomainException — ожидаемые невосстановимые ошибки (v1.0.0)

DomainException — ожидаемые невосстановимые ошибки (v1.0.0)

DomainException — базовый класс для ожидаемых невосстановимых ошибок.

Зачем нужен базовый класс

Базовый класс используется чтобы:

  1. отличать ожидаемые ошибки от неожиданных;

  2. централизованно задавать поля/атрибуты, которые нужны для логгирования и формирования ответа пользователю;

  3. иметь опорную точку для маппинга на протокол точки входа (например HTTP).

Рекомендуемое содержимое

В базовый класс обычно добавляются:

  1. внутреннее сообщение об ошибке;

  2. код ошибки;

  3. сообщение для пользователя или ключ сообщения в локализациях;

  4. первопричина (cause).

Код ошибки (errorCode) используется как стабильный идентификатор ошибки во внешних интеграциях и/или при формировании HTTP-ответов.

Пример реализации

open class DomainException(
    message: String? = null,
    cause: Throwable? = null,
    errorCode: String? = null,
    val userMessageKey: String? = null,
    val attributes: Map<String, Any?> = emptyMap(),
) : Exception(message, cause) {

    val errorCode: String = errorCode ?: errorCodeOf(this::class)

    companion object {

        inline fun <reified E : Throwable> errorCodeOf(): String =
            errorCodeOf(E::class)

        fun errorCodeOf(clazz: kotlin.reflect.KClass<out Throwable>): String =
            requireNotNull(errorCodeOrNullOf(clazz)) {
                "Can't derive error code from class: ${clazz.qualifiedName ?: clazz}"
            }

        fun errorCodeOrNullOf(clazz: kotlin.reflect.KClass<out Throwable>): String? =
            clazz.simpleName
                ?.camelToKebabCase()
                ?.removeSuffix("-exception")

    }

}

private fun String.camelToKebabCase(): String = buildString(length + 8) {
    this@camelToKebabCase.forEachIndexed { index, ch ->
        val isUpper = ch in 'A'..'Z'
        if (isUpper && index != 0) append('-')
        append(if (isUpper) ch.lowercaseChar() else ch)
    }
}

EntityNotFoundException — стандартная not found ошибка

EntityNotFoundException — стандартная ожидаемая невосстановимая ошибка для случаев, когда сущность/ресурс не удаётся найти по ключу.

Когда использовать

Используйте EntityNotFoundException, когда:

  1. входные данные содержат идентификатор сущности;

  2. по этому идентификатору сущность не находится в репозитории/хранилище;

  3. операция должна завершиться отказом (например вернуть не 2xx в HTTP и откатить транзакцию).

Когда не использовать

Не используйте EntityNotFoundException, когда код загрузил данные из собственных хранилищ, извлёк из них идентификатор и затем попытался загрузить сущность по этому идентификатору, но не нашёл. Такие ошибки являются логическими ошибками программы и сигнализируются стандартными механизмами (например error или checkNotNull).

Что важно сохранить в ошибке

Как минимум стоит включать:

  1. тип сущности;

  2. список ключей (имя поля → значение);

  3. стабильный код ошибки (errorCode).

Пример реализации

class EntityNotFoundException(
    val type: String,
    val keys: List<Pair<String, Any?>>,
    override val message: String = "Entity of type $type not found by ${keys.format()}",
    errorCode: String = "entity-not-found",
    cause: Throwable? = null
) : DomainException(
    message,
    cause,
    errorCode = errorCode
) {

    init {
        require(keys.isNotEmpty())
    }

    constructor(prop: KProperty1<*, Any?>, key: Any) : this(
        prop.parameters[0].type.jvmErasure.simpleName!!,
        listOf(prop.name to key),
        errorCode = "${prop.parameters[0].type.jvmErasure.simpleName!!.inKebabCase}-not-found"
    )

    constructor(entityType: KClass<*>, key: Any) : this(
        entityType.simpleName ?: "Entity",
        listOf("key" to key),
        errorCode = "${entityType.simpleName?.inKebabCase ?: "entity"}-not-found"
    )

}