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:
response.statusText
→ only returns generic messages like Bad Request.response.error()
→ doesn’t exist at all, so it throwsTypeError: response.error is not a function
.- My custom JSON error (
{"error":"my Custom Error"}
) was ignored completely.
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:
- Show messages directly in the page (not just console).
- Disable the button while the request is running (to prevent double-clicks).
- 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.