Handle JWT Token Expiration Gracefully
Implement graceful JWT token expiration handling with refresh patterns, retry logic, and proactive renewal. Code examples for Node.js, Python, and browser apps.
JWT token expiration is not a bug - it is a security feature. The real problem is when your application crashes or logs users out unexpectedly instead of handling expiration gracefully. This guide covers production-ready patterns for detecting, refreshing, and proactively renewing JWT tokens.
Common errors covered
API returns 401 but app shows generic error
HTTP 401 Unauthorized
{"error": "token_expired", "message": "The access token has expired"}
Your API client catches the 401 but does not distinguish between an expired token (recoverable via refresh) and an invalid token (requires re-login). Users see a cryptic error instead of being prompted to log in again.
Step-by-step fix
- 1 Decode the failing token with the JWT Decoder to confirm expiration.
-
2
Check the
expclaim - convert it with the Timestamp Converter to see when it expired. - 3 Implement a 401 interceptor that attempts token refresh before surfacing errors.
- 4 Only show login prompt after refresh also fails.
// Generic error handling
fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } })
.then(res => { if (!res.ok) throw new Error('Request failed'); })
.catch(err => showError('Something went wrong'));
// Graceful expiration handling with refresh
async function fetchWithAuth(url) {
let res = await fetch(url, { headers: authHeaders() });
if (res.status === 401) {
const refreshed = await refreshAccessToken();
if (refreshed) {
res = await fetch(url, { headers: authHeaders() });
} else {
redirectToLogin();
return;
}
}
return res;
}
Multiple simultaneous refresh requests cause token invalidation
Error: Refresh token has already been used
{"error": "invalid_grant", "error_description": "Token has been revoked"}
When multiple API calls fail simultaneously due to token expiration, each triggers its own refresh request. If the server uses single-use refresh tokens, the second refresh attempt invalidates the first new token, creating a cascade failure.
Step-by-step fix
- 1 Identify if multiple 401 responses are triggering parallel refresh calls.
- 2 Implement a refresh mutex - only one refresh at a time.
- 3 Queue other requests to wait for the refresh to complete.
- 4 Retry all queued requests with the new token.
// Each failed request refreshes independently
async function apiCall(url) {
const res = await fetch(url, { headers: authHeaders() });
if (res.status === 401) {
await refreshToken(); // Race condition!
return fetch(url, { headers: authHeaders() });
}
}
// Singleton refresh with request queue
let refreshPromise = null;
async function getValidToken() {
if (isTokenExpired()) {
if (!refreshPromise) {
refreshPromise = refreshToken().finally(() => refreshPromise = null);
}
await refreshPromise;
}
return getStoredToken();
}
Token expires during long-running operations
Error: Token expired mid-upload
HTTP 401 at 87% of file upload
Long operations (file uploads, batch processing, report generation) may outlast the token lifetime. The token is valid when the operation starts but expires before it finishes.
Step-by-step fix
-
1
Check the token's
expclaim before starting long operations. - 2 If the token expires within the operation's expected duration, refresh proactively.
- 3 Implement a token renewal timer that refreshes before expiration.
- 4 Use the JWT Decoder to inspect your token's lifetime during development.
// No proactive check
async function uploadLargeFile(file) {
const res = await fetch('/upload', {
method: 'POST',
headers: authHeaders(),
body: file, // May take 10+ minutes
});
}
// Proactive token renewal
async function uploadLargeFile(file) {
const token = getDecodedToken();
const expiresIn = token.exp - Math.floor(Date.now() / 1000);
if (expiresIn < 600) { // Less than 10 minutes left
await refreshToken();
}
const res = await fetch('/upload', {
method: 'POST',
headers: authHeaders(),
body: file,
});
}
Prevention Tips
- Refresh tokens proactively when less than 20% of their lifetime remains.
- Use a singleton refresh pattern to prevent race conditions with parallel requests.
- Set access token lifetime to 15-60 minutes and refresh token to 7-30 days.
-
Add a
tokenRefreshedevent that all API callers can subscribe to for seamless updates.
Frequently Asked Questions
Should I store JWT tokens in localStorage or cookies?
For web apps: use HttpOnly secure cookies for refresh tokens (XSS protection) and short-lived access tokens in memory. Never store refresh tokens in localStorage.
How do I handle token expiration in mobile apps?
Use the same refresh pattern as web apps. Store refresh tokens in the device keychain (iOS) or encrypted shared preferences (Android). Proactively refresh on app resume.
What if my refresh token also expires?
Prompt the user to re-authenticate. This is expected behavior for long-idle sessions. Consider using sliding refresh token expiration (reset on use) for better UX.
Related Error Guides
Related Tools
Still stuck? Try our free tools
All tools run in your browser, no signup required.