5. A Generic Fetch Handler
The Fetch Helper
The code for fetching data is almost always the same:
In a
try
block,fetch
from a URL and parse the response as JSONIn a
catch
block, log the caughterror
. Any error that occurs in thetry
block will be caught by this one sharedcatch
block
const fetchData = async (url, options = {}) => {
try {
// pass along the options to fetch
const response = await fetch(url, options);
// Throw an error if the response was not 2xx - let the catch statement handle it
if (!response.ok) throw new Error(`Fetch failed. ${response.status} ${response.statusText}`)
// Guard clause: make sure that the content type of the response is JSON before reading it
const contentType = response.headers.get('content-type');
if (contentType === null || !contentType.includes('application/json')) {
// If the contentType of the response is not JSON, read the stream as plain text
const textData = await response.text();
return textData;
}
// Otherwise read the stream as JSON
const jsonData = await response.json();
return jsonData;
}
catch (error) {
// if there was an error, log it and return the error
console.error(error.message);
return error;
}
}
The function below does the following:
It accepts a
url
and anoptions
argument allowing other types of requests to be made (POST, PATCH/PUT, DELETE, etc...). If the caller offetchData
does not provideoptions
, it will default to an empty object.If the
!response.ok
guard clause is triggered, an error is thrown instead of returning. This let's us handle4xx
and5xx
responses in thecatch
block and treat them the same as errors thrown byfetch
andresponse.json()
.It checks the content type of the
response
to determine how to parse (withresponse.json()
orresponse.text()
)
Why Are We Checking the Content Type?
We've added in one detail that can occasionally trip us up: the data is NOT in JSON format. DELETE
requests, for example, often do not return ANY data in response. In other cases, we may be fetching other data formats. In those cases, we can't use the response.json
function and instead will use response.text
to read the incoming response body ReadableStream
.
So, to make our function more flexible, we check the response.headers
to determine the contentType
and then use response.json()
if we're dealing with JSON, and response.text()
if we're dealing with anything else.
A Useful Improvement
One subtle change will make the function more easier to use.
const fetchData = async (url, options = {}) => {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`Fetch failed. ${response.status} ${response.statusText}`)
const contentType = response.headers.get('content-type');
if (contentType === null || !contentType.includes('application/json')) {
const textData = await response.text();
// Return a "tuple", making error handling on the "receiving" end easier
return [textData, null]
}
// Return a "tuple", making error handling on the "receiving" end easier
const jsonData = await response.json();
return [jsonData, null]
}
catch (error) {
// if there was an error, log it and return a tuple: [data, error]
console.error(error.message);
return [null, error];
}
}
This version returns the data in a "tuple" format — an array with 2 values where the first value is always the data (if present) and the second value is always the error (if present). Only one of the two values will ever be present.
Why return a tuple?
You may be wondering, why couldn't we write this helper function such that it just returns the data if there are no errors, or returns the error if there is one?
The reason we don't do this is to make the code that uses this function cleaner. The code that uses fetchData
will need to know if the data it receives is an error or JSON data.
The problem is that error
objects and the jsonData
can often be difficult to differentiate. An error
object will have a message
property, and often times, so do JSON response objects! Take the Dog Image API as an example. It will return its data like this:
{
"message": "https://images.dog.ceo/breeds/mix/dog3.jpg",
"status": "success"
}
Since error
objects and jsonData
objects can look so similar in their structure, we can't simply check the structure of the returned object to know if it was an error or JSON data. We could do something like this:
const getDogImage = async () => {
const dogImage = await fetchData('https://dog.ceo/api/breeds/image/random')
console.log(dogImage.message); // I can't know if this is going to be an error message or a dog picture.
// I could check the type of the object
if (dogImage instanceof Error) {
// handle the error
}
else {
// use the dog image
}
}
But if our fetchData
function always returns a [data, error]
tuple where one of those values will ALWAYS be null
while the other is defined, then the code that uses fetchData
will become much cleaner:
const getDogImage = async () => {
// either dogImage or error will be null
const [dogImage, error] = await fetchData('https://dog.ceo/api/breeds/image/random')
if (error) {
// if the error exists at all, that means that dogImage is null and we should handle the error
// We can render a message to the user that something went wrong without crashing the program
}
else {
// otherwise, the error must be null and we can render the dogImage
}
}
Last updated