2024-10-20
How we handle errors in our applications has a big impact on reliability and developer experience. Poor error handling leads to silent failures, confusing user messages, and hours spent debugging. Good error handling makes problems visible, provides useful context, and keeps the application in a recoverable state.
The fail-fast principle means that when something goes wrong, we should detect and report it immediately rather than continuing with bad data. For example, if a function receives an invalid argument, it is better to throw an error right away than to let the invalid value propagate through the system and cause a confusing failure later.
function processOrder(order) {
if (!order || !order.items || order.items.length === 0) {
throw new Error("Cannot process an order with no items");
}
// proceed with valid order
}Validating inputs at the boundary of our system, whether it is an API endpoint, a function parameter, or user input, catches problems early when the context is clear.
Distinguishing between different types of errors helps us handle them appropriately.
Operational errors are expected runtime problems like a network timeout, a file not found, or a database connection failure. These are not bugs. We should anticipate them and handle them gracefully, for example by retrying the request or showing a user-friendly message.
Programming errors are bugs in our code, like accessing a property on null or passing the wrong type to a function. These should not be caught and silently ignored. They should surface so we can fix the underlying bug.
In React applications, an uncaught error in a component can crash the entire application. Error boundaries prevent this by catching errors in the component tree and displaying a fallback UI instead of a blank screen.
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
logErrorToService(error, info);
}
render() {
if (this.state.hasError) {
return <p>Something went wrong. Please try again.</p>;
}
return this.props.children;
}
}We can wrap different sections of the application in separate error boundaries so that a failure in one part does not take down the whole page.
When building APIs, consistent error responses make life easier for frontend developers. A good error response includes a meaningful status code, an error message, and optionally an error code that the client can use to display a specific message.
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Account balance is too low to complete this transaction"
}
}Using standard HTTP status codes correctly is important. 400 for bad requests, 401 for unauthenticated, 403 for forbidden, 404 for not found, and 500 for unexpected server errors.
Catching errors is only useful if we also record them. Logging errors with enough context, such as the request parameters, user ID, and stack trace, makes debugging much faster. Services like Sentry or Datadog capture errors in production and alert the team when something goes wrong. Without proper logging and monitoring, errors in production can go unnoticed for days.
One of the worst patterns is catching an error and doing nothing with it. An empty catch block hides problems and makes them extremely difficult to diagnose. If we catch an error, we should either handle it meaningfully, log it, or rethrow it. Never ignore it silently.