GPP
GPP

Reputation: 2325

Is there a best practice ("right way") for inspecting fetch response object in vanilla html/css/js app?

Background

I cooked up a very basic (inline js in the <script> tag!) web app that makes a rest API request of an API I also implemented.

I'm a novice to webdev and figured that I should inspect the response.status but in doing so I noticed the same thing numerous other posters have: that the fetch() function returns either an object that contains status and Header properties with an empty body or just the response body if the .json() function was invoked on response.json()

My question: I wanted to use the response.status in a separate piece of logic/function outside of the async function I wrapped fetch in to action on whether or not to display an error message in some inline HTML or the parsed expected outcome. However, as you probably figured, the response object only contains the body if .json() was called, and an empty/weird Promise-y object if not.

This design to fetch() made wonder if my use-case for response.status was considered a "best practice" or is the "industry approach" to simply just evaluate the response body, and if the response body is anything other than the expected schema, then handle that as an error?

Related secondary question

In trying to learn more about the fetch and async function behavior, I read that chaining an invoked async function with a .then().catch() expression is irregular (maybe even an anti pattern?) - why would this be the case if MDN's own async docs use this syntax in their own tutorials?

link: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#supplying_request_options

More backstory

After trawling dozens of stackoverflow posts like this one, I realized I could combine the response.json() object and the response.status kvp into a new object with this code:

// we have to await the .json() which is a function that returns a promise
const respObj = await response.json();

//now, create and return a 'resp' object that has all desired response elements:
const resp = {
 "status":response.status,
 "body":respObj
};
return resp;

Upvotes: 2

Views: 1450

Answers (1)

jfriend00
jfriend00

Reputation: 707876

If you want access to elements of the response besides just the parsed JSON, then by all means, you can create your own function that calls fetch() and uses some logic and then returns multiple pieces of the response data. I would personally probably not repackage the response data structure, but rather just leave it intact so the caller can get anything they want from there including headers, etc...

Here's an example:

async function fetchJSON(...args) {
    const response = await fetch(...args);
    // check for non 2xx status
    if (!response.ok) {
         throw new Error(`Response status ${response.status}`, { cause: response });
    }
    const data = await response.json();
    return { data, response };
}

This function returns a promise that resolves to an object that contains both the parsed JSON data and the original response object. This allows the caller to check on anything they want to in the original response object.

If response.ok is not true, then it will reject with an error object that also contains the original response object.

My question: I wanted to use the response.status in a separate piece of logic/function outside of the async function I wrapped fetch in to action on whether or not to display an error message in some inline HTML or the parsed expected outcome. However, as you probably figured, the response object only contains the body if .json() was called, and an empty/weird Promise-y object if not.

That's not really the right way to think of it. Both fetch() and response.json() return promises. The promise that fetch() returns will resolve to the response object. The promise that response.json() returns will resolve to the parsed JSON data object. If you use await with each of these, then you can use the promise to just directly get the resolved result (as shown in my example above).

This design to fetch() made wonder if my use-case for response.status was considered a "best practice" or is the "industry approach" to simply just evaluate the response body, and if the response body is anything other than the expected schema, then handle that as an error?

It is common to check response.ok because a 4xx or 5xx status does not reject the promise from fetch(). If you're specifically looking for a 3xx error, then you may write specific code to check for that. response.ok will tell you if it is a 2xx status (usually what you want when requesting data). You can skip this check and "assume" that downstream when you try to read/parse the JSON, you will get an error if there was no JSON (and this is the case, but it will likely be a JSON parse error, not an indication of the real problem that it wasn't a 2xx status that returned the desired data. So I find it better to specifically check response.ok so I know I'm only trying to parse JSON if the request was deemed successful (from my point of view).

You will find that third party browser libraries that offer a bit more convenient interface such as axios() don't even make you handle two separate steps in the process. You just make a request and they return data or an error. That's what my small fetchJSON() function above does too. It handles the internal checks itself. If the response doesn't contain JSON or contains invalid JSON, it will reject and your code can check the resulting error object to see why it rejected.

In trying to learn more about the fetch and async function behavior, I read that chaining an invoked async function with a .then().catch() expression is irregular (maybe even an anti pattern?) - why would this be the case if MDN's own async docs use this syntax in their own tutorials?

So, you generally don't want to mix lots of async/await and .then() and .catch() within the same function as the flow of control can get confusing to follow. If you mark the function async, then inside it you're probably using await instead of .then() because the whole point of an async function is to enable the simpler flow of control by using await. There are occasional cases where I find a .catch() to be simpler and less code that a try/catch around an await when I want to handle the error locally, but that's not a very common case for me.

I wouldn't go so far to call it an anti-pattern to occasionally find a situation where .catch() is simpler than try/catch around an await, but that's really just personal preference. But, I would call it an anti-pattern to be freely mixing await and .then() all over an async function. That just makes the code more complicated to follow. In general, you want to pick one style or the other for your code within a given function.

Upvotes: 1

Related Questions