Search code examples
javascriptpromisefetch

How to "nest" javascript promise?


I wrote a function to catch non-200 results with fetch:

 1 function $get(url, callback) {
 2  fetch(url, {credentials: "same-origin"})
 3    .then(resp => {
 4      if (!resp.ok) {
 5        resp.text().then((mesg) => {
 6          throw {"stat": resp.status, "mesg": mesg.trim()}
 7        })
 8        return resp.text()
 9      } 
10      return resp.json() 
11    })
12    .then(data => callback({"stat": 200, "data": data}))
13    .catch(error => callback(error))
14}

I got error on line 9:

ERROR: TypeError: Failed to execute 'text' on 'Response': body stream already read

The reason that I have to write code shown in line 5~7 is that if I wrote:

if (!resp.ok) {
  throw {"stat": resp.status, "mesg": resp.statusText}
return resp.json()

I will get error message like {"stat": 403, "mesg": "Forbidden"}, while what I want is: {"stat": 403, "mesg": "invalid user name or password"}.

On the server side my go program will generate non-200 reply like this:

> GET /api/login?u=asdf&p=asdf HTTP/1.1
> Host: localhost:7887
> User-Agent: curl/7.68.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 403 Forbidden
< Content-Type: text/plain; charset=utf-8
< X-Content-Type-Options: nosniff
< Date: Sat, 17 Jul 2021 11:53:16 GMT
< Content-Length: 25
< 
invalid username or password

I.e. the go library do not modify http status text, instead, put error message in body, which maybe mandated by the http standard (e.g. status text cannot be changed).

So, my question is, either:

  • How to read the body of non-200 reply without using promise?
  • or, how to reply an "empty" promise in case of error, to prevent the stream being read again?

=== EDIT ===

The following code works OK, however, it seems to use "anti" pattern as pointed out by comments:

function $get(url, callback) {
  fetch(url, {credentials: "same-origin"})
    .then(resp => {
      if (!resp.ok) {
        resp.text().then((mesg) => {
          callback({"stat": resp.status, "mesg": mesg.trim()})
        })
        return new Promise(function(_, _) {}) 
      } 
      return resp.json()
    })
    .then(data => callback({"stat": 200, "data": data}))
    .catch(error => { console.log(`GET ${url}\nERROR: ${error}`) })
}

However, this doe not work:

function $get(url, callback) {
  fetch(url, {credentials: "same-origin"})
    .then(resp => {
      if (!resp.ok) {
        resp.text().then((mesg) => {
          throw `{"stat": resp.status, "mesg": mesg.trim()}`
        }) 
      } 
      return resp.json()
    })
    .then(data => callback({"stat": 200, "data": data}))
    .catch(error => { console.log(`GET ${url}\nERROR: ${error}`) })
}

The throw will generate this error, instead of passing control to the catch below:

127.0.0.1/:1 Uncaught (in promise) {"stat": resp.status, "mesg": mesg.trim()}

Solution

  • Considering you are using fetch you can also use async/await and do the following. :

    async function $get(url, callback) {
      try {
        const resp = await fetch(url, {credentials: "same-origin"});
        
        if (!resp.ok) {
          // this will end up in the catch statement below
          throw({ stat: resp.status, mesg: (await resp.text()).trim());
        }
        
        callback({ stat: 200, data: await resp.json() });
      } catch(error) {
        callback(error);
      }
    }
    

    I don't understand why you would use callback functions though :) those are so 1999


    To explain your error, you are calling resp.text() twice when there is an error. To prevent that, you should immediately return the promise chained from the first resp.text() call. This will also throw the error and end up in the catch block without reaching the consecutive then() statement:

    function $get(url, callback) {
     fetch(url, {credentials: "same-origin"})
       .then(resp => {
         if (!resp.ok) {
           return resp.text().then((mesg) => {
    //     ^^^^^^
             throw {stat: resp.status, error: mesg.trim()}
           });
         } 
         return resp.json() ;
       })
       .then(data => callback({stat: 200, data }))
       .catch(error => callback(error))
    }
    

    A "proper" $get function which doesn't use callbacks:

    function $get(url) {
      return fetch(url, {credentials: "same-origin"}).then(async (resp) => {
        const stat = resp.status;
    
        if (!resp.ok) {
          throw({ stat, error: (await resp.text()).trim() });
        }
    
        return { stat, data: await resp.json() };
      });
    }
    

    Which you can consume like:

    $get('https://example.com')
      .then(({ stat, data }) => {
    
      })
      .catch({ stat, error}) => {
    
      })