Synchronous Error Handling
Objectives
By the end of this lesson, you should be able to:
- Explain what happens, by default, when synchronous code inside a route throws
- Catch a synchronous error with
try/catchand respond appropriately - Explain why letting Express’s default error page reach a client is a real problem
💡 Why this matters: Real code fails, invalid input, a bad calculation, a missing property. What a route does when that happens, crash with an ugly default page, or respond cleanly, is the difference between a fragile app and a production-ready one.
⚠️ A note on verification: every snippet and every response shown below was actually run and tested with real HTTP requests.
What Happens by Default
app.get('/sync-error', (req, res) => {
JSON.parse('{invalid json');
res.send('never reached');
});
curl -i http://localhost:4401/sync-error
HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
at JSON.parse (<anonymous>)
at /tmp/node108/server1.js:5:8
at Layer.handleRequest (...)
...
Express automatically catches a synchronous throw inside a route handler, and responds with a 500, so a single bad request doesn’t crash the entire server. But the default error page includes the full error message and stack trace, exact file paths, line numbers, internal library details, this is a real information leak, never appropriate to show a real client, and this default is exactly what the rest of this module replaces.
Catching It Properly
app.get('/sync-safe', (req, res) => {
try {
JSON.parse('{invalid json');
res.send('never reached');
} catch (err) {
console.log('caught:', err.message);
res.status(400).json({ error: 'Invalid input' });
}
});
curl -w " [%{http_code}]" http://localhost:4402/sync-safe
{"error":"Invalid input"} [400]
try/catch (Module 2) works exactly the same way inside a route handler as anywhere else in JavaScript, catch the error, log whatever detail is useful server-side (console.log/console.error), and send back a clean, appropriate response, a 400 here, since the actual problem was invalid input, not a server failure.
Choosing the Right Status Code
Not every caught error is the client’s fault. Invalid input, a malformed request body, a value failing validation, deserves a 4xx status code (Module 5), the client can fix it by sending something different. A genuine server-side failure, a bug, an unexpected null, a downstream service failing, deserves a 5xx, the client did nothing wrong, retrying the identical request might work once the server-side issue is fixed. Choosing correctly here (rather than defaulting everything to 500) is part of what makes an API predictable to use, covered fully in Module 9.
Try It
- Write a route that throws a synchronous error (an intentional bug, like accessing a property on
undefined) with notry/catch, and confirm Express’s default error page appears, including the stack trace. - Wrap the same code in
try/catch, log the error server-side, and return a clean400or500JSON response instead. - Write a route parsing a value from
req.query(withparseIntorJSON.parse) that could reasonably fail on bad input, and handle the failure with an appropriate status code and message. - Explain, in your own words, why exposing a full stack trace to a real client is a security concern, not just an aesthetic one.
Recap
- Express automatically catches a synchronous throw and returns a
500, but its default error page leaks internal details (stack traces, file paths) that should never reach a real client. try/catchinside a route handler catches the error explicitly, letting the handler log detail server-side and respond cleanly.- The status code should reflect who’s at fault,
4xxfor a bad request the client can fix,5xxfor a genuine server-side failure.
Next lesson: async error handling, why a rejected promise inside a route needs special care.