Site icon FSIBLOG

How to Fix the PHP Error Message in JavaScript with Fetch

How to Fix the PHP Error Message in JavaScript with Fetch

How to Fix the PHP Error Message in JavaScript with Fetch

When I was working on a Symfony project, I ran into a common frustration: I wanted my backend (PHP/Symfony) to send custom error messages to the frontend and then display those messages in JavaScript using fetch.

On the surface, it looked simple. But the reality was different: fetch didn’t give me the custom error message. Instead, it only returned the generic HTTP status text like Bad Request. Let me show you what went wrong and how I fixed it.

The Problem Code

Here’s the first version of my code:

PHP (Symfony Controller)

#[Route('/test', name:'test', methods: ['POST'])]
public function test(Request $req): Response
{
    return new JsonResponse(['error' => 'my Custom Error'], 400);
}

This looks good I’m returning a JSON error with status code 400.

JavaScript

let btn = document.getElementById('myButton');
btn.addEventListener('click', function(event){
  const fd = new FormData();
  fd.append('user', 'myUserName');

  fetch('/test', {method: 'POST', body: fd})
    .then((response) => {
      if (!response.ok) {
        throw Error(response.statusText); 
        //  Only gives back "Bad Request"
        //  Does not include my JSON error
      }
      return response.json();
    })
    .then((data) => {
      console.log('data received', data);
    })
    .catch((error) => {
      console.log(error);
    });
});

The Error

The mistake became clear:

The .catch block never got the real backend error message, only the generic status. That wasn’t helpful.

The Fix

The key was this: even when the response is an error, I can still read its body. That means I should call response.json() before throwing an error.

Here’s the improved version:

let btn = document.getElementById('myButton');
btn.addEventListener('click', function(event){
  const fd = new FormData();
  fd.append('user', 'myUserName');

  fetch('/test', {method: 'POST', body: fd})
    .then(async (response) => {
      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.error || 'Unknown error occurred');
      }
      return response.json();
    })
    .then((data) => {
      console.log('data received:', data);
    })
    .catch((error) => {
      console.error(' Server error:', error.message);
    });
});

Example Output

If Symfony returned:

{"error": "my Custom Error"}

Then my JavaScript console now correctly shows:

 Server error: my Custom Error

Finally, the backend message made it through to the frontend.

Extra Practice Functionality

I didn’t want to stop there. I wanted this code to be user-friendly, not just developer-friendly. So, I added some enhancements:

  1. Show messages directly in the page (not just console).
  2. Disable the button while the request is running (to prevent double-clicks).
  3. Handle both success and error cases cleanly.

Here’s the enhanced version:

let btn = document.getElementById('myButton');
let output = document.getElementById('output');

btn.addEventListener('click', async function(event){
  const fd = new FormData();
  fd.append('user', 'myUserName');

  btn.disabled = true; 
  output.textContent = " Processing...";

  try {
    const response = await fetch('/test', { method: 'POST', body: fd });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.error || 'Unknown error');
    }

    const data = await response.json();
    output.textContent = " Success: " + JSON.stringify(data);
  } catch (error) {
    output.textContent = "Error: " + error.message;
  } finally {
    btn.disabled = false; 
  }
});

Example HTML

<button id="myButton">Send</button>
<p id="output"></p>

Now, instead of looking at the console, users immediately see feedback right on the page.

Final Thought

This little debugging journey taught me a valuable lesson fetch won’t magically give you custom server errors you have to extract them yourself. By parsing the response body (response.json()) even when the request fails, I now have full control over how my Symfony backend communicates with the frontend. And by adding small UI touches (like disabling the button and showing messages), I created a better experience for users.

Exit mobile version