defensive fetch response
In 2023, while deep in the trenches of my engineering manager role, I decided to purchase a license to access react.gg, “The interactive way to master modern React”. I followed ui.dev team’s work for the last few years, and they often released material that’s been quite helpful to learn React and modern Javascript. This course is just another perspective for brushing up on what I know and pushing myself to learn more.
In the course, I came across a comment reply made showing a good way to use fetch()
. The following was initially asked:
I am flummoxed by the assertion that this code is not wise:
function getGithubProfile(username) { return fetch( `https://api.github.com/users/${username}` ).then((res) => res.json()); }
What is the correct way to obtain data from an external source? In other languages, I’ve been using functions for decades to retrieve data from external sources.
His response included a defensive approach to fetching data:
async function getGithubProfile(username) {
let response;
try {
response = await fetch(`https://api.github.com/users/${username}`);
} catch (error) {
throw new Error(`Network error: ${error.message}`);
}
if (!response.ok) {
const message = `HTTP error! status: ${response.status}`;
throw new Error(message);
}
let data;
try {
data = await response.json();
} catch (error) {
throw new Error(`API response error: ${error.message}`);
}
if (!data || typeof data !== 'object') {
throw new Error('Invalid response format from the API');
}
return data;
}
While I’ve used and seen plenty of snippets from this function in various places, this was the first time I saw it constructed like this. Using fetch like this is one of a few ways that this function could be written, but it’s also very readable and easy to understand.
Here are the four things this structure is doing:
- Using asynchronous code to fetch data and return a Promise using
async/await
- Using
try...catch
statement to allow the fetch and catch responses or error - Throw errors for Network issues, response status issues, API issues, or response formatting issues
- Safely returns the data fetched from the URL or API
Now, let me attempt to destructure this defensive code block.
try…catch
First, we need to fetch some data, so we’ll access an external URL, this example from Github.
async function getGithubProfile(username) {
let response;
try {
response = await fetch(`https://api.github.com/users/${username}`);
} catch (error) {
throw new Error(`Network error: ${error.message}`);
}
/* more code below */
}
First, we declare a variable called response
that’ll be referenced and reassigned throughout the function. This might seem kind of redundant considering it’s called almost immediately inside of the try
block. Interestingly enough, we have to declare response
outside of the try
block because of block scoping, which applies to using const
and let
. If we declared var response
inside of try {}
, because of hoisting, it would actually work in the function. Theo (t3.gg) made a video going more in depth about the constraints of the try
block.
The first call of response
is in the above referenced try...catch
syntax construct containing the code to fetch the data or return a network error. Using both async
to convert the function to be asynchronous Promise as well as the await
expression to suspend execution until the returned promise is fulfilled or rejected is one of the best modern features of Javascript. If the response does not work properly, an error will display to let the user know there’s an issue.
validate the data
Before we even work continue to work with a response containing data, let’s make sure the fetch request was okay by returning a HTTP status code of 200, representing a successful response.
async function getGithubProfile(username) {
/* fetch the data */
if (!response.ok) {
const message = `HTTP error! status: ${response.status}`;
throw new Error(message);
}
/* more code below */
}
If the response was anything but okay, this conditional will throw an error with the status code.
json the data
Time to convert the response to a readable format for Javascript
async function getGithubProfile(username) {
/* fetch the data */
/* response validation */
let data;
try {
data = await response.json();
} catch (error) {
throw new Error(`API response error: ${error.message}`);
}
/* more code below */
}
After setting a new variable for the data, we resolve the response to a JavaScript object. This object could be anything that can be represented by JSON — an object, an array, a string, a number, etc. Now the fetched data is ready to be used by javascript.
On the other hand, if there’s an error with the returned data for any reason, such as malformed JSON, it’ll once again throw a new error.
Note: It’s also here that you might need a different resolve type. For example, there are cases where you’re working with a string response that isn’t or doesn’t need to be JSON formatted data. In that case, your response could be returned using await response.text()
to return a string. There are several other method types that can be applied here as well.
type validation
What happens if we get through everything but the data itself doesn’t actually resolve to a valid object or the data is just plain corrupt? This is an often forgotten step in fetching data where we should do one last check to make sure we’re actually returning a valid data type, object in our case, or just making sure the data is still available. If not, throw an API error.
async function getGithubProfile(username) {
/* fetch the data */
/* response validation */
/* resolve data type */
if (!data || typeof data !== 'object') {
throw new Error('Invalid response format from the API');
}
/* more code below */
}
return the data
At this point, the data has survived multiple check stops to make sure the result of the data is exactly what we need. So, all we have to do is return the data.
return data;
Voila! Now we have a fully functioning and very safe function to fetch data for our site or application.
For React, this type of function can either stand alone outside of a component to be invoked in the Component, or it can also exist directly in React.useEffect()
to grab the data when the component mounts or updates based on a state change.
fetch promise
By default, fetch()
is really a promise, allowing you to cover a lot of these checks that are located inside of the multiple try...catch
blocks. Here’s an alternative syntax, often referred to as then/catch
for fetching:
fetch('https://api.github.com/users/${username}')
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw 'Error getting users list'
}
}).then((data) => {
/* handle data */
}).catch((error) => {
/* handle errors */
});
Both methods are Promise chains, both handle responses and errors in similar ways. There’s nothing wrong with using the above, it’s perfectly valid! Why use try...catch
over then/catch
built-in promise syntax?
The primary difference is the addition of the async/await
in ES2017. Ultimately, using this syntax offers a cleaner to read, easier to understand method to retrieve the data. There’s no nesting of then/catch
methods, which can be common in then/catch
blocks.
Here’s a great Smashing Magazine article going over examples between try...catch
over then/catch
.
Happy fetching!
Updates
- August 14, 2024 – Added additional context around the
try
block and a video link to YouTube