error-handling
Error handling patterns. Exception hierarchies, Result types, structured error responses, retry strategies, circuit breakers.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Error Handling
Golden Rule: Never Catch Generic Exceptions
// NEVER
try { processPayment(order) }
catch (e: Exception) { logger.error("Failed", e) }
// DO — catch specific exceptions
try { processPayment(order) }
catch (e: PaymentDeclinedException) { order.markFailed(e.reason) }
catch (e: PaymentGatewayTimeoutException) { scheduleRetry(order) }
The ONLY place for catching Exception is @ControllerAdvice global handler.
Exception Hierarchy
// Business exceptions (4xx — client can fix)
abstract class BusinessException(
val errorCode: String, val httpStatus: HttpStatus, message: String
) : RuntimeException(message)
// System exceptions (5xx — infrastructure problem)
abstract class SystemException(
val errorCode: String, message: String, cause: Throwable
) : RuntimeException(message, cause)
Result<T> for Expected Failures
sealed interface OperationResult<out T> {
data class Success<T>(val value: T) : OperationResult<T>
data class Failure(val error: DomainError) : OperationResult<Nothing>
}
sealed interface DomainError {
data class NotFound(val resource: String, val id: String) : DomainError
data class ValidationFailed(val violations: List<String>) : DomainError
data class Conflict(val message: String) : DomainError
}
Structured Error Responses with Correlation ID
Every error response MUST include correlationId from MDC for distributed tracing.
Retry Rules
- ONLY retry on transient infrastructure errors
- NEVER retry on business exceptions (4xx)
- NEVER retry non-idempotent operations without idempotency key
- Always use exponential backoff with jitter
- Max 3-5 attempts
Circuit Breaker (Resilience4j)
CLOSED → (failure threshold 50%) → OPEN
OPEN → (wait 30s) → HALF_OPEN
HALF_OPEN → (test calls succeed) → CLOSED
Distributed System Error Handling
Saga Pattern (Choreography)
For multi-service operations where each step must be compensable on failure.
Order Service Payment Service Inventory Service
| | |
|--- OrderCreated ----->| |
| |--- PaymentCharged --->|
| | |--- StockReserved ---> DONE
| | |
| (if payment fails) |
|<-- PaymentFailed ----| |
|--- OrderCancelled |
| |
| (if stock fails) |
| |<-- StockFailed -------|
| |--- PaymentRefunded |
|<-- OrderCancelled ----| |
// Each service defines its action + compensation
data class SagaStep<T>(
val name: String,
val action: suspend () -> T,
val compensation: suspend (T) -> Unit
)
// Orchestration-based saga coordinator
class SagaOrchestrator {
private val completedSteps = mutableListOf<Pair<String, Any>>()
suspend fun <T> execute(step: SagaStep<T>): T {
return try {
val result = step.action()
completedSteps.add(step.name to result as Any)
result
} catch (e: Exception) {
logger.error("Saga step '${step.name}' failed, compensating...")
compensateAll()
throw SagaRollbackException("Saga failed at '${step.name}'", e)
}
}
private suspend fun compensateAll() {
// Compensate in reverse order
completedSteps.asReversed().forEach { (name, result) ->
try {
// invoke stored compensation
logger.info("Compensating: $name")
} catch (e: Exception) {
logger.error("Compensation failed for '$name' — manual intervention required", e)
// Alert ops team — this is a critical inconsistency
}
}
}
}
Rules:
- Every action MUST have a compensation (no "fire and forget" in sagas)
- Compensations must be idempotent — they may be retried
- Log all saga steps with correlation ID for audit trail
- If compensation fails → alert ops team, do NOT silently swallow
Outbox Pattern (Reliable Event Publishing)
Guarantee that domain events are published even if the message broker is temporarily down.
// 1. Write event to outbox table within the same transaction as the domain change
@Transactional
fun createOrder(request: CreateOrderRequest): Order {
val order = orderRepository.save(Order.from(request))
outboxRepository.save(OutboxEvent(
aggregateType = "Order",
aggregateId = order.id,
eventType = "OrderCreated",
payload = objectMapper.writeValueAsString(order),
status = "PENDING"
))
return order
}
// 2. Scheduled job polls outbox and publishes to broker
@Scheduled(fixedDelay = 1000)
@Transactional
fun publishPendingEvents() {
val events = outboxRepository.findByStatus("PENDING")
events.forEach { event ->
try {
messageBroker.publish(event.eventType, event.payload)
event.status = "PUBLISHED"
} catch (e: Exception) {
event.retryCount++
if (event.retryCount > MAX_RETRIES) event.status = "FAILED"
}
outboxRepository.save(event)
}
}
Dead Letter Queue (DLQ) Handling
@Component
class DlqHandler {
@KafkaListener(topics = ["orders.DLQ"])
fun handleDeadLetter(record: ConsumerRecord<String, String>) {
logger.error("DLQ message: topic=${record.topic()}, key=${record.key()}")
// 1. Log for investigation
dlqRepository.save(DlqEntry(
originalTopic = record.headers().lastHeader("original-topic")?.value()?.let { String(it) },
key = record.key(),
payload = record.value(),
errorReason = record.headers().lastHeader("error-reason")?.value()?.let { String(it) },
receivedAt = Instant.now()
))
// 2. Alert if DLQ depth exceeds threshold
val depth = dlqRepository.countByOriginalTopicAndResolvedFalse(record.topic())
if (depth > DLQ_ALERT_THRESHOLD) {
alertService.notify("DLQ depth for ${record.topic()} is $depth — investigate")
}
}
}
.NET Exception → Java Exception Mapping
Reference for teams migrating .NET exception handling to Java/Kotlin.
Exception Hierarchy Mapping
| .NET Exception | Java/Kotlin Equivalent | Notes |
|---|---|---|
Exception |
Exception |
Base class |
SystemException |
RuntimeException |
Unchecked |
ApplicationException |
RuntimeException (custom) |
Deprecated in .NET too |
ArgumentNullException |
IllegalArgumentException |
Or Kotlin's require() |
ArgumentOutOfRangeException |
IllegalArgumentException |
With range info in message |
InvalidOperationException |
IllegalStateException |
Object in wrong state |
NotImplementedException |
NotImplementedError |
Kotlin built-in |
NotSupportedException |
UnsupportedOperationException |
|
NullReferenceException |
NullPointerException |
Kotlin null safety prevents most |
KeyNotFoundException |
NoSuchElementException |
|
HttpRequestException |
RestClientException (Spring) |
HTTP client errors |
TaskCanceledException |
CancellationException (coroutines) |
Async cancellation |
TimeoutException |
java.util.concurrent.TimeoutException |
|
DbUpdateException (EF) |
DataAccessException (Spring) |
Database errors |
DbUpdateConcurrencyException |
OptimisticLockingFailureException |
|
ValidationException |
ConstraintViolationException (Jakarta) |
Bean Validation |
Pattern Migration
// .NET pattern
try {
await _orderService.CreateOrderAsync(request);
}
catch (DbUpdateException ex) when (ex.InnerException is PostgresException pgEx && pgEx.SqlState == "23505") {
throw new ConflictException($"Order already exists: {request.OrderId}");
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.ServiceUnavailable) {
throw new ServiceUnavailableException("Payment gateway unavailable", ex);
}
// Kotlin equivalent
try {
orderService.createOrder(request)
} catch (e: DataIntegrityViolationException) {
// Spring wraps database constraint violations
throw ConflictException("Order already exists: ${request.orderId}", e)
} catch (e: RestClientException) {
throw ServiceUnavailableException("Payment gateway unavailable", e)
}
Key Differences
-
.NET exception filters (
catch when): No direct Kotlin equivalent — usewhenexpression inside catch block -
.NET
AggregateException: Kotlin coroutines throwCancellationException; usesupervisorScopefor partial failure handling -
.NET
ExceptionDispatchInfo.Capture(): Kotlin uses standardthrow— stack trace preserved automatically -
Global handler: .NET
IExceptionFilter→ Spring@ControllerAdvice+@ExceptionHandler
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-03-11
- Index updated
- 2026-03-11
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!