Welcome to the first lesson of Building Robust API Clients in Kotlin. In this course, you will learn how to handle common scenarios when working with APIs more effectively. One of the key aspects of working with APIs is error handling. Handling errors gracefully not only helps in building robust applications but also enhances user experience by providing meaningful feedback when things go wrong. Our goal in this lesson is to help you manage these outcomes effectively.
When you send a request to an API, the server responds with an HTTP status code. These codes indicate the result of your request. Understanding them is essential for effective error handling. Here's a brief overview:
- 2xx (Success): Indicates that the request was successfully received, understood, and accepted. For example, a
200
status code means OK. - 4xx (Client Errors): Suggests that there was an error in the request made by your client. For example,
404
means the requested resource was not found. - 5xx (Server Errors): Indicates that the server failed to fulfill a valid request. A common code here is
500
, which means an internal server error.
By paying attention to these codes, you can determine whether your request succeeded or if there was a problem that needs addressing.
Kotlin's OkHttp
library is a popular choice for handling HTTP requests and responses. It provides a more modern and flexible approach compared to HttpURLConnection
. We can use OkHttp
to check the response code and throw exceptions for unsuccessful status codes.
Consider the following example, which fetches todo items from an API using OkHttp
:
Kotlin1import okhttp3.OkHttpClient 2import okhttp3.Request 3import java.io.IOException 4 5// Base URL for the API 6val baseUrl = "http://localhost:8000" 7 8// Function to fetch all todos with error handling 9fun fetchTodos() { 10 val client = OkHttpClient() 11 12 // Create a request object with the /todos endpoint 13 val request = Request.Builder() 14 .url("$baseUrl/todos") 15 .build() 16 17 try { 18 // Execute the request and get the response 19 client.newCall(request).execute().use { response -> 20 // Check if the response code is 4xx or 5xx and throw an exception if so 21 if (!response.isSuccessful) { 22 throw IOException("HTTP error occurred: ${response.code} ${response.message}") 23 } 24 25 // If no exception was thrown, print success message 26 println("Todos fetched successfully!") 27 } 28 } catch (e: IOException) { 29 // Handle any HTTP or other errors that occur 30 println(e.message) 31 } 32} 33 34// Call the function to fetch todos 35fetchTodos()
In this example, we use OkHttpClient
to create and execute a request. We then check the isSuccessful
property of the response to determine if the status code indicates an error (4xx
or 5xx
). If an error is detected, an IOException
is thrown, making error handling straightforward.
The use {}
block in the examples is crucial for ensuring that the response is closed properly, thereby preventing resource leaks. This is particularly important when dealing with resources like network responses, as failing to close them can lead to memory leaks and other resource management issues.
Following our discussion on raising exceptions for unsuccessful status codes, let's delve into specific scenarios where errors might occur. In this first example, a GET request is sent to a non-existent route, leading to an HTTP error because a 404 Not Found
status code is returned.
Kotlin1fun fetchInvalidRoute() { 2 val client = OkHttpClient() 3 4 // Create a request object with a non-existent endpoint 5 val request = Request.Builder() 6 .url("$baseUrl/invalid-route") 7 .build() 8 9 try { 10 // Execute the request and get the response 11 client.newCall(request).execute().use { response -> 12 // Check if the response code is 4xx or 5xx and throw an exception if so 13 if (!response.isSuccessful) { 14 throw IOException("HTTP error occurred: ${response.code} ${response.message}") 15 } 16 } 17 } catch (e: IOException) { 18 // Handle any HTTP or other errors that occur 19 println(e.message) 20 } 21} 22 23// Call the function to fetch from an invalid route 24fetchInvalidRoute()
This will produce the following output indicating that the requested resource was not found:
Plain text1HTTP error occurred: 404 Not Found
Continuing with error handling, the next scenario involves sending a POST request without a required field, the 'title', resulting in an HTTP error due to a 400 Bad Request
.
Kotlin1fun postWithoutTitle() { 2 val client = OkHttpClient() 3 4 // Create a request object with the /todos endpoint 5 val requestBody = "{}".toRequestBody("application/json".toMediaType()) 6 val request = Request.Builder() 7 .url("$baseUrl/todos") 8 .post(requestBody) 9 .build() 10 11 try { 12 // Execute the request and get the response 13 client.newCall(request).execute().use { response -> 14 // Check if the response code is 4xx or 5xx and throw an exception if so 15 if (!response.isSuccessful) { 16 throw IOException("HTTP error occurred: ${response.code} ${response.message}") 17 } 18 } 19 } catch (e: IOException) { 20 // Handle any HTTP or other errors that occur 21 println(e.message) 22 } 23} 24 25// Call the function to post without a title 26postWithoutTitle()
The following output shows a 400 Bad Request
error, indicating missing required fields:
Plain text1HTTP error occurred: 400 Bad Request
Finally, let's examine how to handle broader request-related issues. This example demonstrates a scenario where an exception occurs due to connectivity issues or other problems external to the HTTP response itself.
Kotlin1fun fetchWithInvalidUrl() { 2 val client = OkHttpClient() 3 4 // Create a request object with an invalid URL 5 val request = Request.Builder() 6 .url("http://invalid-url") 7 .build() 8 9 try { 10 // Execute the request and get the response 11 client.newCall(request).execute().use { response -> 12 // Check if the response code is 4xx or 5xx and throw an exception if so 13 if (!response.isSuccessful) { 14 throw IOException("HTTP error occurred: ${response.code} ${response.message}") 15 } 16 } 17 } catch (e: IOException) { 18 // Handle any HTTP or other errors that occur 19 println("Other error occurred: ${e.message}") 20 } 21} 22 23// Call the function to fetch with an invalid URL 24fetchWithInvalidUrl()
When a connection cannot be established, the following output will provide details about the connectivity issue:
Plain text1Other error occurred: no protocol: http://invalid-url
These examples build on the principles of exception handling we previously discussed, offering more detailed insights into managing errors effectively in different contexts within your API interactions.
In this lesson, you learned about the importance of error handling in API requests and were introduced to effective techniques using HTTP status codes and try-catch blocks. These practices are crucial for creating robust applications that deal with errors gracefully and provide clear feedback to users.
You are now equipped to practice these skills through hands-on exercises that will reinforce the concepts you've learned. As you move forward, you'll continue to build on these techniques to engage with more advanced API features. Remember, practicing error handling is key — experiment with different scenarios to see how errors are managed and how they affect your applications.
