How do I catch and parse errors properly in my React form submission handler?

Viewed 903

I'm using React 16.13.0. I have the following function to deal with submit events:

handleFormSubmit(e) {
  e.preventDefault();
  const NC = this.state.newCoop;
  delete NC.address.country;

  fetch('/coops/',{
      method: "POST",
      body: JSON.stringify(this.state.newCoop),
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
  }).then(response => {
      if (response.ok) {
          return response.json();
      }
      console.log(response.json());
      console.log(response.body);
      throw new Error(response.statusText);
  }).catch(errors => {
      console.log(errors);
      this.setState({ errors });
  });
}

However, I'm having an issue getting the errors properly from the response. When an error occurs, my endpoint returns a 400 request with the error text. This is what happens in curl:

curl --header "Content-type: application/json" --data "$req" --request POST "http://localhost:9090/coops/"
{"phone":["The phone number entered is not valid."]}

But the response.statusText contains "400 (Bad Request)". What's the right way to catch the error text and preserve it for future parsing? If my endpoint needs to format the data differently, what should it do (using Django/Python 3.7)?

Edit:

This is the Input component in which I'm trying to display errors:

<Input inputType={'text'}
    title = {'Phone'}
    name = {'phone'}
    value = {this.state.newCoop.phone}
    placeholder = {'Enter phone number'}
    handleChange = {this.handleInput}
    errors = {this.state.errors}
/> 

And the code of the input component, src/Input.jsx

import React from 'react';
import {FormControl, FormLabel} from 'react-bootstrap';

const Input = (props) => {
  return (
    <div className="form-group">
      <FormLabel>{props.title}</FormLabel>
      <FormControl
          type={props.type}
          id={props.name}
          name={props.name}
          value={props.value}
          placeholder={props.placeholder}
          onChange={props.handleChange}
      />

      {props.errors && props.errors[props.name] && (
          <FormControl.Feedback>
              <div className="fieldError">
                  {props.errors[props.name]}
              </div>
          </FormControl.Feedback>
      )}
    </div>
  )
}

export default Input;

When I run console.log(errors) they appear as such:

{phone: Array(1), web_site: Array(1)}
5 Answers

The Response.ok property API states that:

Response.ok Read only

A boolean indicating whether the response was successful (status in the range 200–299) or not.

That means that even response.ok is false, the response.json() will return the data.

Body.json()

Takes a Response stream and reads it to completion. It returns a promise that resolves with the result of parsing the body text as JSON.

So, in your code, you should define your first fetch resolve to asynchronous and if the response it's not ok, then throw with the resolved response.json() using await:

handleFormSubmit(e) {
  e.preventDefault();
  const NC = this.state.newCoop;
  delete NC.address.country;

  fetch('/coops/',{
      method: "POST",
      body: JSON.stringify(this.state.newCoop),
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
  }).then(async response => { // Define the first resolve to an asynchronous function
      if (response.ok) {
          // If it's OK, resolve JSON and return the actual data
          return await response.json();
          // or better set react state
          // const data = await response.json();
          // this.setState({ data });
      } else {
          // It's not OK, throw an error with the JSON data
          // so you'll be able to catch
          throw await response.json();
      }
  }).catch(errors => {
      // Here you should get the actual errors JSON response
      console.log(errors);
      this.setState({ errors });
  });
}

You can check the test example working using fetch-mock in this Stackblitz workspace.

If my endpoint needs to format the data differently, what should it do (using Django/Python 3.7)?

You'll have to let us know more about how your endpoint handles the requests by providing some code and explanation.

UPDATE

Regarding your component and displaying the errors, the result JSON returns an array of errors for each field. If you'll only one error, then change the endpoint to return a string instead of an array or display just the first error. If you'll have multiple errors, then you can map and render through all errors array for each field:

const Input = (props) => {
  return (
    <div className="form-group">
      <FormLabel>{props.title}</FormLabel>
      <FormControl
          type={props.type}
          id={props.name}
          name={props.name}
          value={props.value}
          placeholder={props.placeholder}
          onChange={props.handleChange}
      />

      // If you just want to display the first error
      // then render the first element of the errors array
      {props.errors && props.errors[props.name] && (
        <FormControl.Feedback>
          <div className="fieldError">
            {props.errors[props.name][0]}
          </div>
        </FormControl.Feedback>
      )}

      // Or if you may have multiple errors regarding each field
      // then map and render through all errors
      {/*
      {props.errors && props.errors[props.name] && (
        <FormControl.Feedback>
          {props.errors[props.name].map((error, index) => (
            <div key={`field-error-${props.name}-${index}`} className="fieldError">
              {error}
            </div>
          ))}
        </FormControl.Feedback>
      )}
      */}
    </div>
  )
}

Actually, the fetch API is a little different from others. you should pass another .then() to the get the data and then analyze it, and using several callbacks make codes hardly to read, I use async/await to handle the error:

async handleFormSubmit(e) {
  e.preventDefault();
  const NC = this.state.newCoop;
  delete NC.address.country;

  try {
    const response = await fetch('/coops/',{
      method: "POST",
      body: JSON.stringify(this.state.newCoop),
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
    });

    if (response.ok) {
      const result = await response.json();
      console.log('_result_: ', result);
      return result;
    }

    throw await response.json();

  } catch (errors) {

    console.log('_error_: ', errors);
    this.setState({ errors });
  }
}

Update for your new question:

Definitely, it is another question, that why the error doesn't appear, in fact, the phone error is and JavaScript Array, and you should show it by its way like below code, I use restructuring assignment for props:

import React from 'react';
import { FormControl, FormLabel } from 'react-bootstrap';

const Input = ({
  title,
  type,
  name,
  value,
  placeholder,
  handleChange,
  errors,
}) => (
  <div className="form-group">
    <FormLabel>{title}</FormLabel>
    <FormControl
      type={type}
      id={name}
      name={name}
      value={value}
      placeholder={placeholder}
      onChange={handleChange}
    />
    {errors && errors[name] && (
      <FormControl.Feedback>
        {errors[name].map((err, i) => (
          <div key={err+i} className="fieldError">{err}</div>
        ))}
      </FormControl.Feedback>
    )}
  </div>
);

export default Input;

You are not really explaining what you want to do with the response. But based on your use for throw new Error I assume you want the following .catch call to handle it. In the below solution, errors would be assigned the object from the JSON response.

response.json() returns a promise. If you want to "throw" that value as an error you have to do something like this (instead of throw new Error(...)):

return response.json().then(x => Promise.reject(x))

Returning a rejected promise from a .then callback causes the promises returned by said .then call to be rejected as well.

In context:

 fetch('/coops/',{
        method: "POST",
        body: JSON.stringify(this.state.newCoop),
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
    }).then(response => {
        if (response.ok) {
            return response.json();
        }
        return response.json().then(x => Promise.reject(x));
    }).catch(errors => {
        console.log(errors);
        this.setState({ errors });
    });

Note: Since you are not doing anything with the return value of a successful response, the return response.json() inside the if statement is not necessary. You could rewrite that call to:

.then(response => {
  if (!response.ok) {
    return response.json().then(x => Promise.reject(x));
  }
})

If my endpoint needs to format the data differently, what should it do (using Django/Python 3.7)?

Since we don't know what structure your component expects there is not much we can suggest. –

let the catch part catch failure errors and then catch the errors you need in the then part

  }).then(response => {
       //use a switch to got through the responses or an if statement 
        var response = JSON.Parse(response)
        //here am assuming after you console log response you gt the correct array placement
        if(response[0]==200){
           // this indicates success
              return  JSON.stringify({"success":"true","message":"success message";})
         }else if(response[0]==400){
           // this indicates error
              return  JSON.stringify({"success":"false","message":"Error message";})
           }
  }).catch(errors => {
      console.log(errors);
      this.setState({ errors });
  });

on the other end you could check the first entry in the array to determine whether the query was success or failure , then handle message appropriately

With the fetch API Response objects represent the Response meta - connection information, headers, that kind of thing.

At this stage the body has not been read.

response.statusText is the from the HTTP Response Status line, eg 200 Ok or 400 (Bad Request) in your case.

response.ok is a helper for checking if response.status >= 200 && response.status < 300

So far nothing we have looked at here is the contents of the response.

For that you need to use response.json(), response.text(), or response.blob(). As long as there is a body any of these functions can be used to read it - regardless of the HTTP Status Code.

Each of these functions read the body of sent to you by the server and does some kind of additional processing on it (check MDN or your preferred source of documentation for more information).

So in your case - to read and do something with the error returned to your by the server I would look at something like

fetch(...)
    .then(response => {
        if (response.ok) {
            return response.json();
        }
        if (response.status === 400) {
            return Promise.reject(response.json());
        }
        return Promise.reject(new Error(`Unexpected HTTP Status: ${response.status} - ${response.statusText}`));
    })
    .then(...)
    .catch(...);
Related