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.
When building REST APIs, proper error handling is essential for:
To follow this tutorial, you'll need:
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")
}
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
)
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)
}
}
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)
}
}
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")
}
}
}
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)
}
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)
}
}
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)
}
}
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:
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!
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
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
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