0

I have a fetch() call in function myFetch() (returns the fetch's return value), and would like to wrap it in another function callApi(). My goal is to make callApi() return a Promise, such that:

  • A "resolve" state indicates success (the fetch call returned response.ok / HTTP 200).
    • In this case, I would like the value (of promise) to be the body/text of the response.
  • A "reject" state indicates any failure during the process, which could be:
    • There is something wrong with the fetch call, e.g. a GET request with body.
      • In this case, I would like the message of reject to be that error.
    • The fetch succeeded, but the upstream response is not HTTP 200.
      • In this case, I would like the message of reject to be status_code: body where status_code and body are both from the HTTP response.
      • In fact, it does not have to be in this form, as long as the caller of callApi() can reconstruct this same message.

However, I do not know how to write the callApi() function. My attempt only ended up with this (where myFetch() is the call to fetch):

    return new Promise(
        (resolve, reject) => {
            myFetch()
                .then((response) => {
                    if (response.ok) {
                        resolve(response);
                    } else {
                        reject(`${response.status}: ${response.text}`);
                    }
                }, (error) => {
                    reject(error);
                });
        }
    );

As you see, response.text is a Promise, but I need the string there. How can I achieve my goal?

In addition, if I also need to receive JSON response (from the fetch), will there be any difference?

renyuneyun
  • 608
  • 5
  • 18
  • 1
    `myFetch` is already a Promise, using Promise constructor on a Promise is an anti-pattern.. – Keith May 25 '23 at 18:32
  • Avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! – Bergi May 26 '23 at 22:31

2 Answers2

1

You shouldn't create a new promise with new Promise when you already have a promise -- the one returned by myFetch.

Secondly, you'll need to call the text method on the response, and construct the string when that returned promise resolves.

It may be easier to use async await syntax:

async function myApi() {
    try {
        const response = await myFetch();
        const body = await response.text();
        return response.ok ? body : `${response.status}: ${body}`;
    } catch(e) {
        return e;
    }
}    

Here the returned promise will never reject. If you want the promise to reject unless the response status is ok, then leave out the try..catch wrapper, and do:

async function myApi() {
    const response = await myFetch();
    const body = await response.text();
    if (!response.ok) throw `${response.status}: ${body}`;
    return body;
}

When you want to use the json method instead of text, then you'd want to produce an object instead of a string, and then it might be a better idea to always return an object that has the same toplevel keys:

The version that will never return a promise that rejects:

async function myApi() {
    let ok = false, error = true, status;
    try {
        const response = await myFetch();
        ({ ok, status }) = response;
        body = await response.json();
        error = false;
    } catch(e) {
        body = e;
    }
    return { ok, error, status, body };
}    

The version that will reject the promise when the response is not ok:

async function myApi() {
    let ok = false, error = true, status;
    try {
        const response = await myFetch();
        ({ ok, status }) = response;
        body = await response.json();
        error = false;
    } catch(e) {
        body = e;
    }
    if (!ok) throw { error, status, body };
    return { error, status, body };
}    
trincot
  • 317,000
  • 35
  • 244
  • 286
  • Thanks for the answer and additional information. My rationale of not using async/await syntax was unsure about the behaviour when rejected, i.e. case 2.1 in the original question. In particular, my understanding is that fetch will always return a Promise in the cases I illustrated. Then will your suggestion (1) result in a Promise that is rejected with the original rejection message from fetch? Or will it be a Promise with the error as value? Or will it be an error (that my caller should catch)? – renyuneyun May 26 '23 at 19:50
  • This function will return a promise that will never reject but always fulfil. In the first code snippet, if an error is caught, that promise will resolve with the error object as value. In the second code, that resolved value will be an object of which the body property has the error object. If you want the promise to reject, then do `throw` instead of `return`. – trincot May 26 '23 at 20:30
  • I added an alternative for each of the two solutions, where the promise will reject when the response status is not ok. – trincot May 26 '23 at 20:39
  • Thanks for that. I actually want them to reject when errors happen, as stated in the original question. After reading other articles, e.g. https://stackoverflow.com/questions/33445415/javascript-promises-reject-vs-throw, I think the second snippet seems to be what I need. It was good to know that I can throw a thing that is not an Error -- some hidden knowledge that deviates from my experience with other languages with stronger types. – renyuneyun Jun 08 '23 at 16:40
  • Missing bits for me: JS can throw strings (or other data as well?); after encountering an error in async (promise), it will reject with the error as the value. – renyuneyun Jun 08 '23 at 16:43
  • 1
    Yes, you can throw [anything](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/throw#description), and the promise returned by an `async` function will be rejected [when an uncaught error occurs or is raised](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function#return_value) in its code – trincot Jun 08 '23 at 16:52
-1

To obtain the response body as a string, you can modify your code as follows:

  myFetch()
    .then((response) => {
      if (response.ok) {
        response.text().then((body) => {
          resolve(body);
        });
      } else {
        response.text().then((body) => {
          reject(`${response.status}: ${body}`);
        });
      }
    })
    .catch((error) => {
      reject(error);
    });
});

response.text() returns a Promise that resolves to the response body as a string. By chaining another .then() callback to it, you can access the body and pass it to either resolve or reject based on the response's ok status.

As for the JSON responses, you might want to use response.json() method instead of response.text(). This method returns a Promise, which resolves to the parsed JSON data instead of just text. Here's an updated version of the code that handles JSON responses:

return new Promise((resolve, reject) => {
  myFetch()
    .then((response) => {
      if (response.ok) {
        response.json().then((data) => {
          resolve(data);
        });
      } else {
        response.json().then((errorData) => {
          reject({
            status: response.status,
            body: errorData,
          });
        });
      }
    })
    .catch((error) => {
      reject(error);
    });
});

When the response isn't successful, the reject function is called with an object containing the response status and the parsed JSON error body. This allows the caller of callApi() to access both the status code and the response body in a structured manner.

More information:

  1. Fetch API: MDN Fetch API
  2. Response.text(): MDN Response.text()
  3. Response.json(): MDN Response.json()
  4. JavaScript Promises: MDN Promises
  • Thanks very much! This looks like what I'm looking for. Just one additional question: won't the `then()` chain return without waiting for the decision of resolved or rejected? Then how can the caller correctly obtain the result? – renyuneyun May 26 '23 at 19:53
  • Ah, forget my silly further question. The resolve/reject will be called asynchronously, while the caller will wait that Promise until they are called. – renyuneyun May 26 '23 at 19:56