1. Overview

Exception handling is one of the most important topics to cover when building REST APIs. Spring provides great support for handling exceptions with native features that can help to customize the response of the API.

In this tutorial, we’ll explore some approaches to implementing the exception handling in a Spring Boot REST API with Kotlin.

2. REST Controller and Template Message

2.1. Example REST Controller

In the first place, we need to create an example API. For our case study, we’ll define a small REST controller exposing services to create, update, and retrieve articles:

@RestController
@RequestMapping("/articles")
class ArticleController(val articleService: ArticleService) {

    @PostMapping()
    fun createArticle(@RequestBody title: String): ArticleModel {
        return articleService.createArticle(title);
    }

    @GetMapping()
    fun getArticle(@RequestParam id: String): ArticleModel {
        return articleService.getArticle(id);
    }

    @PutMapping()
    fun updateArticle(@RequestParam id: String, @RequestParam title: String): ArticleModel {
        return articleService.updateArticle(id, title);
    }
}

2.2. Custom Template Message

Secondly, we’ll define a template for the error messages of the API:

class ErrorMessageModel(
    var status: Int? = null,
    var message: String? = null
)

The status property stores the number of the HTTP status code, and the message property represents a custom message defined to describe the exception thrown.

3. Exception Controller Advice

Using the annotation @ControllerAdvicewe can define a global handler for multiple controllers. For example, we might want to return a custom message for errors of the type IllegalStateException:

@ControllerAdvice
class ExceptionControllerAdvice {

    @ExceptionHandler
    fun handleIllegalStateException(ex: IllegalStateException): ResponseEntity<ErrorMessageModel> {

        val errorMessage = ErrorMessageModel(
            HttpStatus.NOT_FOUND.value(),
            ex.message
        )
        return ResponseEntity(errorMessage, HttpStatus.BAD_REQUEST)
    }
}

Additionally, we can define custom exceptions for specific scenarios. For instance, we can create an exception when articles are not found by the API:

class ArticleNotFoundException(message: String) : RuntimeException(message) {
}

Accordingly, this exception will be caught by the global handler:

@ExceptionHandler
fun handleArticleNotFoundException(ex: ArticleNotFoundException): ResponseEntity<ErrorMessageModel> {
    val errorMessage = ErrorMessageModel(
        HttpStatus.NOT_FOUND.value(),
        ex.message
    )
    return ResponseEntity(errorMessage, HttpStatus.NOT_FOUND)
}

Finally, we have all the components we need to define the ArticleService class that will perform the business logic of the API:

@Service
class ArticleService {

    lateinit var articles: List<ArticleModel>

    @PostConstruct
    fun buildArticles() {
        articles = listOf(
            ArticleModel("1", "Exception Handling in Kotlin"),
            ArticleModel("2", "Decorator Patter in Kotlin"),
            ArticleModel("3", "Abstract Pattern in Kotlin")
        )
    }

    fun createArticle(title: String): ArticleModel {
        val article = (articles.find { articleModel -> articleModel.title == title })
        if (article != null) {
            throw IllegalStateException("Article with the same title already exists")
        }
        return ArticleModel("4", title)
    }

    fun getArticle(id: String): ArticleModel {
        return articles.find { articleModel -> articleModel.id == id }
            ?: throw ArticleNotFoundException("Article not found")
    }

    fun updateArticle(id: String, title: String): ArticleModel {
        val article = (articles.find { articleModel -> articleModel.id == id }
            ?: throw ArticleNotFoundException("Article not found"))
        if (title.length > 50) {
            throw IllegalArgumentException("Article title too long")
        }
        article.title = title
        return article
    }
}

4. The ResponseStatusException Class

The ResponseStatusException class brings great support to handle exceptions dynamically. Consequently, we can handle errors within the method definition of the controller classes:

@PutMapping()
fun updateArticle(@RequestParam id: String, @RequestParam title: String): ArticleModel {
    try {
        return articleService.updateArticle(id, title);
    } catch (ex: IllegalArgumentException) {
        throw ResponseStatusException(HttpStatus.BAD_REQUEST, ex.localizedMessage, ex)
    }
}

As a result, we don’t need a global controller, but we’d need to define the places where to handle specific exceptions.

5. Conclusion

In this tutorial, we explored some efficient approaches to implementing exception handling in a REST API with Kotlin. We can define global handlers using the @ControllerAdvice annotation and also have dynamic handlers with the ResponseStatusException class.

As always, the complete code for this article is available over on GitHub.