
Error Handling Best Practices in Spring Boot with Kotlin
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
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
- Use Hierarchical Exception Handling
- Create a base exception class
- Extend it for specific error cases
- Handle exceptions at appropriate levels
- 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
- 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
- Include Relevant Information
- Error message
- Timestamp
- Request path
- Correlation ID
- Validation errors (when applicable)
Common Pitfalls to Avoid
- Don't Expose Stack Traces
- Never return stack traces in production
- Log them for debugging instead
- Don't Ignore Exceptions
- Always handle or propagate exceptions
- Log appropriate context
- Don't Mix Concerns
- Keep error handling separate from business logic
- Use aspect-oriented programming when appropriate
- 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!