Table of Contents
hile most React error-handling articles focus on Error Boundaries and try-catch blocks, we'll explore innovative patterns and lesser-known techniques that can transform how you handle errors in React applications.
React applications have become a cornerstone for building dynamic and responsive user interfaces. However, as applications grow in complexity, managing errors gracefully becomes a critical aspect of delivering a seamless user experience. Advanced React error handling goes beyond the basics of catching errors and dives into strategies that ensure your app remains robust, reliable, and user-friendly.
Error occurring is normal in developers' daily coding situations hence handling it and reducing user-side glitches is more important.
In this blog, we will explore advanced react error handling, resolving, and monitoring before that let’s understand the basics of react error handling.
Current Scenario
Error Boundary
Error boundaries are React components that capture JavaScript errors during rendering, in life cycle methods, and within constructors throughout the entire subtree beneath them. You can log these errors and present a fallback user interface.
Key Points
- Prevents full app crashes
- Shows friendly error messages instead of blank screens
- It makes debugging easier with centralized error-catching
Examples
import { ErrorBoundary } from "react-error-boundary";
export default function ReactErrorBoundary(props: any) {
return (
<ErrorBoundary
onError={(error, errorInfo) => {
// log the error
console.log("Error caught!");
console.error(error);
console.error(errorInfo);
}}
onReset={() => {
// reloading the page to restore the initial state
// of the current page
console.log("reloading the page...");
window.location.reload();
}}
>
{props.children}
</ErrorBoundary>
);
}
Advanced Topics
Token Management Functions
When you use a authtoken for authenticating user, that is common way of authentication, in this scenario when that token gets expired on certain time, instead of forcing users to log in again refresh tokens help you quietly get a new access token behind the scenes. In React apps, handling this process well means happier users who stay securely logged in without interruption. Let's explore how to make this work in a way that's both secure and user-friendly.
Let’s understand this with some examples.
Step 1
Initially, we got the token from local storage and set the limit when it expires.
useState(localStorage.getItem("token"));
const REFRESH_TOKEN_EXPIRY_THRESHOLD = 5 * 60 * 1000; // 5 minutes
Step 2
The useEffect hook activates when a component first loads or when the refresh token updates. The checkTokenExpiry function examines if the current refresh token has reached its expiration time. This setup ensures we monitor token status at key moments during the application's lifecycle.
useEffect(() => {
let tokenExpiryTimeout;
const checkTokenExpiry = async () => {
const expiryTime = localStorage.getItem("tokenExpiryTime");
if (!expiryTime) {
console.error("No expiry time found for token.");
return;
}
const timeRemaining = new Date(expiryTime) - new Date();
if (timeRemaining <= 0) {
console.error("Token has already expired.");
alert("Session expired, please log in again.");
return;
}
if (timeRemaining <= REFRESH_TOKEN_EXPIRY_THRESHOLD) {
alert("Your session is about to expire. Refreshing tokens...");
const newExpiryTime = await getNewRefreshToken();
if (newExpiryTime) {
tokenExpiryTimeout = setTimeout(
checkTokenExpiry,
REFRESH_TOKEN_EXPIRY_THRESHOLD
);
}
} else {
tokenExpiryTimeout = setTimeout(
checkTokenExpiry,
timeRemaining - REFRESH_TOKEN_EXPIRY_THRESHOLD
);
}
};
checkTokenExpiry();
return () => clearTimeout(tokenExpiryTimeout);
}, [refreshToken]);
Step 3
The function sends a request to our server at '/api/refresh-token' with your current refresh token. Then, it grabs the new refresh token and its expiry time from the response. Then it takes care of two important tasks: saving the new token in your browser's storage and updating the app's state to reflect these changes.
const getNewRefreshToken = async () => {
try {
const response = await axios.post("/api/refresh-token", { refreshToken });
const { newRefreshToken, expiryTime } = response.data;
localStorage.setItem("refreshToken", newRefreshToken);
localStorage.setItem("tokenExpiryTime", new Date(expiryTime).toISOString());
setRefreshToken(newRefreshToken);
console.log("Tokens refreshed successfully!");
return new Date(expiryTime).toISOString();
} catch (error) {
console.error("Failed to refresh token:", error);
alert("Session expired, please log in again.");
}
};
Step 4
After refreshing the tokens, the checkTokenExpiry function checks again based on the new expiry time.
if (timeRemaining <= REFRESH_TOKEN_EXPIRY_THRESHOLD) {
alert("Your session is about to expire. Refreshing tokens...");
const newExpiryTime = await getNewRefreshToken();
if (newExpiryTime) {
tokenExpiryTimeout = setTimeout(checkTokenExpiry, REFRESH_TOKEN_EXPIRY_THRESHOLD);
}
} else {
tokenExpiryTimeout = setTimeout(checkTokenExpiry, timeRemaining - REFRESH_TOKEN_EXPIRY_THRESHOLD);
}
Handling Network Failure
In this section, we will guide you through effective strategies for managing network failures during ongoing API calls. Understanding this process is crucial for ensuring reliability and maintaining functionality in your applications.
Step 1
Creating a Utility for API Calls with Retry Logic
const fetchWithRetry = async (url, options = {}, retries = 3, delay = 1000) => {
let attempt = 0;
while (attempt < retries) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
} catch (error) {
attempt++;
if (attempt >= retries) {
throw error; // If retries are exhausted, throw the error
}
console.log(`Attempt ${attempt} failed, retrying...`);
await new Promise(res => setTimeout(res, delay)); // Wait before retrying
{/* if after retry api called you want to increase time for next api call */}
delay *= 2;
}
}
};
Explanation of code:
- URL: The API endpoint to fetch
- Options: Optional setting (headers, methods, etc)for the fetch request.
- Retries: The number of retry attempts
- Delay: Time in milliseconds between retries.
Step 2
Using fetchWithRetry in a React Component
Now, let's use this utility in a React component. We'll fetch data inside a useEffect hook and display the results.
onst [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const result = await fetchWithRetry(apiUrl); // function declared in step 1
setData(result);
} catch (err) {
setError(`Failed to fetch data: ${err.message}`);
} finally {
setLoading(false);
}
};
}, [apiUrl]);
Advanced error monitoring
Handling the source map for better bug tracking, we will provide you with how to enable source map tracing in bug monitoring platforms like bugsnag step-by-step
Having error logs like this minified.js:6:1254 will not help you determine where page/component error is happening.
Step 1
Enable Source Maps in Your Build Process
Webpack config file
module.exports = {
mode: "production",
devtool: "source-map", // This generates source maps
entry: "./src/index.js",
output: {
filename: "bundle.min.js",
path: __dirname + "/dist",
},
};
By doing this, it will create two files, bundle.min.js and bundle.min.js.map, that will help with source code mapping.
Step 2
Upload Source Maps to Bugsnag
To upload the source map to bugsnag cloud you need to install one package first
npm install --save-dev bugsnag-sourcemaps
Once installed, you need to host and upload those files.
bugsnag-source-maps upload --api-key YOUR_BUGSNAG_API_KEY --app-version 1.0.0 --minified-file dist/bundle.js --source-map dist/bundle.js.map --minified-url http://example.com/bundle.js
Explanation
- --api-key: Your Bugsnag project API key.
- --minified-url: The URL where the minified file is hosted (it must match the URL in production).
- --source-map:Path to the source map files.
- --minified-file:Path to the minified files.
Step 3
TypeError: Cannot read property 'foo' of undefined
at MyComponent.render (src/components/MyComponent.jsx:42:15)
at callCallback (src/utils/helpers.js:23:5)
at App (src/App.jsx:85:9)
Conclusion
Advanced React error handling encompasses three key strategies: Error Boundaries for graceful component error handling, Token Management for seamless authentication flows, and Network Failure handling with retry logic.
Combined with source map integration for precise error tracking, these patterns create more resilient React applications that maintain a better user experience during errors while providing developers with clear debugging capabilities.
FAQs
- Is it worth the effort to implement refresh tokens with early expiry checks?
Instead of disrupting users with sudden login prompts, the system quietly handles token renewal in the background, ensuring uninterrupted workflow. Since implementing this proactive approach, developers can see a significant decrease in session-related issues and user complaints. - Why does the delay time double between retry attempts (delay *= 2), and is this a recommended practice?
Implementing exponential backoff (doubling delay between retries) is an industry best practice that prevents server overload, allows network issues to resolve, and minimizes congestion. With an initial 1-second delay, subsequent retries occur at 2s and 4s intervals, creating an optimal recovery window. - Why is it important to add a source map in the error monitoring system?
Source maps are crucial for effective error tracking and debugging in production environments because they help solve a common problem: when you see an error like "minified.js:6:1254", it's nearly impossible to trace where the actual error occurred in your original code. This happens because production code is minified and bundled, making error messages cryptic and unhelpful.