Skip to content

Instantly share code, notes, and snippets.

@odewahn
Last active February 27, 2024 09:56
Show Gist options
  • Save odewahn/5a5eeb23279eed6a80d7798fdb47fe91 to your computer and use it in GitHub Desktop.
Save odewahn/5a5eeb23279eed6a80d7798fdb47fe91 to your computer and use it in GitHub Desktop.
Processing errors with Fetch API

I really liked @tjvantoll article Handling Failed HTTP Responses With fetch(). The one thing I found annoying with it, though, is that response.statusText always returns the generic error message associated with the error code. Most APIs, however, will generally return some kind of useful, more human friendly message in the body.

Here's a modification that will capture this message. The key is that rather than throwing an error, you just throw the response and then process it in the catch block to extract the message in the body:

fetch("/api/foo")
  .then( response => {
    if (!response.ok) { throw response }
    return response.json()  //we only get here if there is no error
  })
  .then( json => {
    this.props.dispatch(doSomethingWithResult(json)) 
  })
  .catch( err => {
    err.text().then( errorMessage => {
      this.props.dispatch(displayTheError(errorMessage))
    })
  })

Frankly, I'm horrified that JavaScript let's you throw some random value, rather than an error, but hey, when in Rome...

@sprankhub
Copy link

I am really not a JS guy, but the following should work:

fetch("/api/foo")
  .then( response => {
    if (!response.ok) {
      return response.text().then(text => {
        throw new Error(text)
      });
    }
    return response.json()  //we only get here if there is no error
  })
  .then( json => {
    this.props.dispatch(doSomethingWithResult(json)) 
  })
  .catch( err => {
    this.props.dispatch(displayTheError(err.message))
  })

@Austinmac56
Copy link

fetch("/api/foo")
.then( response => {
if (!response.ok) { throw response }
return response.json() //we only get here if there is no error
})
.then( Text=> {
this.props.dispatch(doSomethingWithResult(Text))
})
.catch( err => {
err.text().then( errorMessage => {
this.props.dispatch(displayTheError(errorMessage))
})
})
This should help

@othneildrew
Copy link

othneildrew commented Apr 1, 2020

This post has been really helpful guys!
Here's my take on a solution that works very well with various types of data. This is similar to @incorelabs, but with my own sick wicked twist.

So to give you some background, I created an API that handles errors and returns an error as an object as many APIs do.
Example error response from server:

{
    "error": "invalid_request",
    "error_description": "The request is missing a required parameter...",
    "hint": "Check the `client_id` parameter",
    "message": "The request is missing a required parameter...."
}

The problem with the other solutions is that I'm not able to throw and return the same error object, I can only reject and return strings that are not entirely helpful and really vague. So I decided to modify the response to first include the status code, text, etc. Here's the complete solution

function makeRequest(url, options) {
    return new Promise((resolve, reject) => {
        fetch(url, options)
            .then(handleResponse)
            .then(response => JSON.parse(response))
            .then((json) => resolve(json))
            .catch((error) => {
                try {
                    reject(JSON.parse(error))
                }
                    catch(e) {
                        reject(error)
                    }
            })
    })
}

function handleResponse(response) {
    return response.json()
        .then((json) => {
            // Modify response to include status ok, success, and status text
            let modifiedJson = {
                success: response.ok,
                status: response.status,
                statusText: response.statusText ? response.statusText : json.error || '',
                response: json
            }
            
            // If request failed, reject and return modified json string as error
            if (! modifiedJson.success) return Promise.reject(JSON.stringify(modifiedJson))

            // If successful, continue by returning modified json string
            return JSON.stringify(modifiedJson)
        })
}



// Then I use it in other files like so. First declare any options, if needed.
const options = {
// your options... method: POST, headers, cors, etc.
}

// Then make the request using the promise function
makeRequest(/oauth/token, options)
            .then((data) => console.log(data)) // do something great with data
            .catch(error => console.log(error)) // do something useful with error

The expected response will always be (Example):

{
                success: true, // or false
                status: 200, // or 400, 401, etc
                statusText: "",
                response: {
                         // original response from server
                }
}

I've tested this with various APIs and it works so far in all cases Also, I'm using fetch from the isomorphic-unfetch package in a NextJS application.

Hope this helps someone!

@Derimos
Copy link

Derimos commented Apr 10, 2020

Nice post, tnx :)
If you want to use message from API as error message.
In my case:
Api will return 400 in case of invalid data sent in request. Response body will contain: {"message":"Some nasty error message!"}. I want to use this message as error. But there is a problem if you want to reject promise. Because Promise.reject will reject immediately, and will not wait for resolving of result.json() for example.

Here is my take if you want to wait for result.json() and then use resulted json data as error.

fetch()
.then((response) => {
    if (response.status === 200) { // Or what ever you want to check
        return Promise.resolve(response.json()); // This will end up in SUCCESS part
    }
    return Promise.resolve(response.json()).then((responseInJson) => { // This will end up in ERROR part
        return Promise.reject(responseInJson.message); //  responseInJson.message = "Some nasty error message!"
    });
})
.then((result) => { // SUCCESS part
    console.log("Success: ", result); // Response from api in json
}, (error) => { // ERROR part
    // Because we rejected responseInJson.message, error will contain message from api. In this case "Some nasty error message!"
    console.log("Error: ", error); 
})
.catch(catchError => {
    console.log("Catch: ", catchError);
})
.finally(() => { ...});

Hope it helps :)

@garagepoort
Copy link

@Derimos My god, thank you! I had no idea why the body was not in the response.
Saved my day

@Blaiski
Copy link

Blaiski commented Sep 17, 2020

I solved this way:

handleSubmit(e) {
  e.preventDefault()

  const body = {
    email: this.state.email,
  }

  let resStatus = 0
  fetch(Config.REST_API_URL + 'users/registration-request', {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  })
  .then(res => {
    resStatus = res.status
    return res.json()
  })
  .then(res => {
    switch (resStatus) {
      case 201:
        console.log('success')
        break
      case 400:
        if (res.code === 'ValidationFailed') {
          // My custom error messages from the API.
          console.log(res.fieldMessages)
        } else {
          console.log('this is a client (probably invalid JSON) error, but also might be a server error (bad JSON parsing/validation)')
        }
        break
      case 500:
        console.log('server error, try again')
        break
      default:
        console.log('unhandled')
        break
    }
  })
  .catch(err => {
    console.error(err)
  })
}

This was helpful, thanks for sharing

@AlessandroGambaro
Copy link

Hi,

i made this small package (code on github) following what I find here and in fetch documentation:

https://www.npmjs.com/package/fd-fetch-wrapper

Maybe this can help

@najibla
Copy link

najibla commented Nov 26, 2020

With Express, I am doing the following

export async function fetchData(token: any, body: any) {
  const result = await fetch(
    `${config.apiUrl}/${config.projectKey}/my-request`,
    {
      method: 'POST',
      headers: {
        Authorization: token,
      },
      body: JSON.stringify(body),
    }
  );

  const json = await result.json();
  if (!result.ok) {
    throw {
      statusCode: result.status,
      ...json
    };
  }
  return json;
}

and in the route I do

router.post(
  '/some-path',
  handleErrorAsync(async (req: Request, resp: Response, _err: Errback) => {
    const data = await fetchData(req.get('authorization'), req.body);
    resp.json(data)
  })
);

And I have an error handling package that does

// async functions errors handler to avoid using try catch for every async route

export const handleErrorAsync = (func: Function) => (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  func(req, res, next).catch((error: Error) => {
    next(error);
  });
};

// middleware to respond with an error when error caught
export function handleError(
  err: any,
  _req: Request,
  resp: Response,
  _next: NextFunction
) {
  if (err) {
    resp.status(err.statusCode || 500).json(err);
  }
}

And of course I add to the router

router.use(handleError);

@ghmcadams
Copy link

This could be simplified with the following:

return fetch(url)
    .then(res => {
        if (!res.ok) {
            return res.json().then(json => { throw json; });
        }
        return res.json();
    })
    .catch(err => {
        // err is not a promise
    });

@AnthonyZJiang
Copy link

I found this is easier to read:

return fetch(url)
    .then(async (res) => {
        if (!res .ok) {
            throw await res.json();
        }
        return response.json();
    })
    .catch(err => {
        // err is not a promise
    });

@szuecs-alex
Copy link

var handlejson = async function(x){
		var t = await x.text();
		try{
			return JSON.parse(t);
		}catch(e){				
			console.log("unparsed:",t);
			return null; //or throw new Expection(e);
		}
	}
fetch(...).then(handlejson)

@ariccio
Copy link

ariccio commented Jan 18, 2023

Oh I bet yall will love this extremely cursed wrapper I wrote for fetch a few years ago, it lets me bubble up the exact network error to the UI. With this monstrosity, I can get the actual error (e.g. ECONNREFUSED when the server isn't running!):

https://github.com/ariccio/COVID-CO2-tracker/blob/main/co2_client/src/utils/FetchHelpers.ts#L312

@commonpike
Copy link

Actually, I think the node-fetch docs propose a nice solution for the OP ?

Instead of throwing a regular Error, they throw a custom extension of Error that contains the response. In the catch clause, they access the response:

https://github.com/node-fetch/node-fetch?tab=readme-ov-file#handling-client-and-server-errors

class HTTPResponseError extends Error {
	constructor(response) {
		super(`HTTP Error Response: ${response.status} ${response.statusText}`);
		this.response = response;
	}
}
...
	if (!response.ok) {
		throw new HTTPResponseError(response);
	}
...
catch (error) {
	console.error(error);
	const errorBody = await error.response.text();
	....
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment