Give API Failures a Consistent Shape
An Express route can fail in two very different ways. A validation failure is expected and should be readable. An unexpected file or programming failure should not expose a stack trace to the browser.
Open server/src/asyncHandler.js and use the wrapper that turns a rejected route promise into an Express error:
export function asyncHandler(handler) {
return function wrappedHandler(req, res, next) {
Promise.resolve(handler(req, res, next)).catch(next);
};
}
The route can now stay focused on its normal success path. If an async operation throws, Express receives the error through next instead of leaving an unhandled rejected promise.
Open server/src/app.js and look at the final error middleware. It preserves deliberate status codes such as 400, 404, and 409, but unexpected errors become a generic 500 response:
const statusCode = Number.isInteger(err.statusCode) ? err.statusCode : 500;
const message =
statusCode >= 500
? "Could not save your changes. Please try again."
: err.message;
Now open client/src/lib/api.js. The request helper handles the other half of the contract:
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new Error(body.error || `Request failed (${response.status})`);
}
The browser therefore gets one normal JavaScript Error whether the server returned a validation message or the network request failed before a response existed.
There is one special success case to handle: HTTP 204 means “no content.” The helper returns null instead of attempting response.json() on an empty response.
Test it
Trigger a normal validation error and confirm the UI displays the server’s message. Then stop the API and try another action. The UI should report that it cannot reach the server rather than failing silently.
Checkpoint
You can now trace an API failure from its origin in an Express route all the way to the message the React UI displays.