Error Handling Best Practices in Spring Boot with Kotlin

December 30, 2024 4 min read Intermediate

Proper error handling is crucial for building robust and maintainable Spring Boot applications. In this comprehensive guide, we'll explore best practices for handling errors in Spring Boot applications using Kotlin, from basic exception handling to advanced error management strategies.

Introduction

When building REST APIs, proper error handling is essential for:

  • Providing clear feedback to API consumers
  • Maintaining security by not leaking sensitive information
  • Ensuring consistency across your application
  • Making debugging easier in production environments

Prerequisites

To follow this tutorial, you'll need:

  • Kotlin 1.9.0 or later
  • Spring Boot 3.2.0 or later
  • Basic understanding of Spring Boot and Kotlin
  • Familiarity with REST APIs

Basic Setup

First, let's set up a basic Spring Boot project with error handling dependencies:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
}

Creating a Standard Error Response

Let's start by defining a standard error response structure:

data class ApiError(
    val status: HttpStatus,
    val message: String,
    val errors: List<String> = emptyList(),
    val timestamp: LocalDateTime = LocalDateTime.now(),
    val path: String? = null
)

Global Exception Handler

Implement a global exception handler to manage errors consistently across your application:

@RestControllerAdvice
class GlobalExceptionHandler {
    
    @ExceptionHandler(Exception::class)
    fun handleAllExceptions(
        exception: Exception,
        request: WebRequest
    ): ResponseEntity<ApiError> {
        val error = ApiError(
            status = HttpStatus.INTERNAL_SERVER_ERROR,
            message = "An unexpected error occurred",
            errors = listOf(exception.localizedMessage),
            path = (request as ServletWebRequest).request.requestURI
        )
        
        // You might want to log the error here
        // For logging best practices, see: https://kotlincraft.dev/articles/kotlin-logging-a-complete-guide-to-better-logging-in-kotlin
        
        return ResponseEntity(error, HttpStatus.INTERNAL_SERVER_ERROR)
    }

    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationErrors(
        exception: MethodArgumentNotValidException,
        request: WebRequest
    ): ResponseEntity<ApiError> {
        val errors = exception.bindingResult
            .fieldErrors
            .map { "${it.field}: ${it.defaultMessage}" }

        val error = ApiError(
            status = HttpStatus.BAD_REQUEST,
            message = "Validation failed",
            errors = errors,
            path = (request as ServletWebRequest).request.requestURI
        )

        return ResponseEntity(error, HttpStatus.BAD_REQUEST)
    }
}

Custom Exceptions

Create specific exceptions for your business logic:

sealed class BusinessException(message: String) : RuntimeException(message)

class ResourceNotFoundException(resourceName: String, id: Any) : 
    BusinessException("$resourceName with id $id not found")

class DuplicateResourceException(resourceName: String, field: String) : 
    BusinessException("$resourceName with this $field already exists")

class InvalidOperationException(message: String) : 
    BusinessException(message)

Add handlers for these custom exceptions:

@RestControllerAdvice
class BusinessExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException::class)
    fun handleResourceNotFound(
        exception: ResourceNotFoundException,
        request: WebRequest
    ): ResponseEntity<ApiError> {
        val error = ApiError(
            status = HttpStatus.NOT_FOUND,
            message = exception.message ?: "Resource not found",
            path = (request as ServletWebRequest).request.requestURI
        )
        return ResponseEntity(error, HttpStatus.NOT_FOUND)
    }

    @ExceptionHandler(DuplicateResourceException::class)
    fun handleDuplicateResource(
        exception: DuplicateResourceException,
        request: WebRequest
    ): ResponseEntity<ApiError> {
        val error = ApiError(
            status = HttpStatus.CONFLICT,
            message = exception.message ?: "Resource already exists",
            path = (request as ServletWebRequest).request.requestURI
        )
        return ResponseEntity(error, HttpStatus.CONFLICT)
    }
}

Using Error Handling in Controllers

Here's how to use these error handling mechanisms in your controllers:

@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {

    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Long): ResponseEntity<User> {
        val user = userService.findById(id) 
            ?: throw ResourceNotFoundException("User", id)
        return ResponseEntity.ok(user)
    }

    @PostMapping
    fun createUser(@Valid @RequestBody request: CreateUserRequest): ResponseEntity<User> {
        try {
            val user = userService.createUser(request)
            return ResponseEntity.status(HttpStatus.CREATED).body(user)
        } catch (e: DataIntegrityViolationException) {
            throw DuplicateResourceException("User", "email")
        }
    }
}

Handling Validation Errors

For request validation (which we covered in detail in our validation guide), add specific error handling:

@ExceptionHandler(ConstraintViolationException::class)
fun handleConstraintViolation(
    exception: ConstraintViolationException,
    request: WebRequest
): ResponseEntity<ApiError> {
    val errors = exception.constraintViolations
        .map { "${it.propertyPath}: ${it.message}" }

    val error = ApiError(
        status = HttpStatus.BAD_REQUEST,
        message = "Validation failed",
        errors = errors,
        path = (request as ServletWebRequest).request.requestURI
    )

    return ResponseEntity(error, HttpStatus.BAD_REQUEST)
}

Security Considerations

When handling errors in production, be careful not to expose sensitive information:

@Profile("prod")
@RestControllerAdvice
class ProductionExceptionHandler {

    @ExceptionHandler(Exception::class)
    fun handleAllExceptions(
        exception: Exception,
        request: WebRequest
    ): ResponseEntity<ApiError> {
        // Log the full error for debugging
        logger.error("Unexpected error", exception)

        // Return a generic error message to the client
        val error = ApiError(
            status = HttpStatus.INTERNAL_SERVER_ERROR,
            message = "An unexpected error occurred",
            path = (request as ServletWebRequest).request.requestURI
        )

        return ResponseEntity(error, HttpStatus.INTERNAL_SERVER_ERROR)
    }
}

Testing Error Handlers

Here's how to test your error handling:

@SpringBootTest
@AutoConfigureMockMvc
class ErrorHandlingTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

    @Test
    fun `when resource not found then returns 404`() {
        mockMvc.perform(get("/api/users/999"))
            .andExpect(status().isNotFound)
            .andExpect(jsonPath("$.status").value("NOT_FOUND"))
            .andExpect(jsonPath("$.message").value("User with id 999 not found"))
    }

    @Test
    fun `when validation fails then returns 400`() {
        val request = CreateUserRequest(email = "invalid-email", name = "")

        mockMvc.perform(
            post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(ObjectMapper().writeValueAsString(request))
        )
            .andExpect(status().isBadRequest)
            .andExpect(jsonPath("$.status").value("BAD_REQUEST"))
            .andExpect(jsonPath("$.errors").isArray)
    }
}

Best Practices

  1. Use Hierarchical Exception Handling
    • Create a base exception class
    • Extend it for specific error cases
    • Handle exceptions at appropriate levels
  2. Implement Proper Logging
    • Log errors with appropriate severity levels
    • Include relevant context but avoid sensitive data
    • Use correlation IDs for request tracking
      For more details on logging, see our Kotlin logging guide
  3. Return Appropriate HTTP Status Codes
    • 400 Bad Request for client errors
    • 404 Not Found for missing resources
    • 409 Conflict for duplicate resources
    • 500 Internal Server Error for server errors
  4. Include Relevant Information
    • Error message
    • Timestamp
    • Request path
    • Correlation ID
    • Validation errors (when applicable)

Common Pitfalls to Avoid

  1. Don't Expose Stack Traces
    • Never return stack traces in production
    • Log them for debugging instead
  2. Don't Ignore Exceptions
    • Always handle or propagate exceptions
    • Log appropriate context
  3. Don't Mix Concerns
    • Keep error handling separate from business logic
    • Use aspect-oriented programming when appropriate
  4. Don't Overuse Try-Catch
    • Let the global handler manage most exceptions
    • Use try-catch only for specific business cases

Conclusion

Proper error handling is crucial for building robust Spring Boot applications. By following these best practices and using the provided examples, you can create a consistent, secure, and maintainable error handling strategy.

Remember to:

  • Define a standard error response format
  • Implement global exception handling
  • Create specific exceptions for business cases
  • Handle validation errors appropriately
  • Consider security implications
  • Test your error handling thoroughly

For more related topics, check out:


To get started right away, you can find the complete source code for this tutorial in our GitHub repository. If you have any questions or run into issues, feel free to open a GitHub issue!

Latest Articles

Error Handling Best Practices in Spring Boot with Kotlin

Error Handling Best Practices in Spring Boot with Kotlin

4 min read rest · Spring Basics

Master Spring Boot error handling in Kotlin with comprehensive examples: learn to implement global exception handlers, custom error responses, and production-ready error handling strategies

Role-Based Access Control (RBAC) in Spring Security with Kotlin

Role-Based Access Control (RBAC) in Spring Security with Kotlin

4 min read Security · rest

Master Role-Based Access Control (RBAC) in Spring Boot applications using Kotlin with practical examples, from basic setup to advanced configurations with method-level security

Implementing API Key Authentication in Spring Boot with Kotlin: A Complete Guide (2024)

Implementing API Key Authentication in Spring Boot with Kotlin: A Complete Guide (2024)

4 min read Security · rest

Learn how to implement secure API key authentication in Spring Boot with Kotlin, including rate limiting, monitoring, and production-ready best practices - complete with practical examples and ready-to-use code