javascript 122 lines · 3 tabs

Transparent JWT Refresh With a Single-Flight Axios Response Interceptor

Shared by codesnips Sep 2026
3 tabs
const STORAGE_KEY = "auth.refreshToken";

let accessToken = null;
let refreshToken = localStorage.getItem(STORAGE_KEY);

const listeners = new Set();

function emit(event) {
  for (const fn of listeners) fn(event);
}

export const tokenStore = {
  getAccessToken() {
    return accessToken;
  },

  getRefreshToken() {
    return refreshToken;
  },

  setTokens({ access, refresh }) {
    accessToken = access;
    if (refresh) {
      refreshToken = refresh;
      localStorage.setItem(STORAGE_KEY, refresh);
    }
    emit("set");
  },

  clear() {
    accessToken = null;
    refreshToken = null;
    localStorage.removeItem(STORAGE_KEY);
    emit("clear");
  },

  subscribe(fn) {
    listeners.add(fn);
    return () => listeners.delete(fn);
  },
};
3 files · javascript Explain with highlit

This snippet shows how an SPA can refresh expired access tokens transparently, so callers never have to think about 401 responses or token lifetimes. The core pattern is a response interceptor that catches an expired-token error, obtains a fresh access token, and replays the original request — while making sure that a burst of concurrent requests triggers only a single refresh call (a "single-flight" refresh).

The tokenStore tab is a small module that owns all token state. It keeps the access and refresh tokens in memory and mirrors the refresh token to localStorage so a page reload can bootstrap a session. Keeping the access token in memory only is a deliberate trade-off: it reduces the XSS blast radius while accepting that a hard refresh needs a silent re-auth. The store exposes getAccessToken, setTokens, and clear, giving the rest of the app one place to read from and one place to invalidate on logout.

The authClient tab is where the interesting concurrency logic lives. A request interceptor stamps every outgoing call with the current bearer token. The response interceptor inspects failures: only a genuine 401 on a request that has not already been retried is eligible for recovery. The key detail is refreshPromise — a module-level promise that acts as a lock. The first 401 calls refreshAccessToken, which stores the in-flight promise; every other request that fails during that window awaits the same promise instead of firing its own refresh. This prevents a thundering herd of refresh calls that would race, rotate the refresh token multiple times, and invalidate each other. Note that the refresh request uses a bare axios instance, not authClient, to avoid an infinite interceptor loop. On success the original config is replayed with the new token; on failure the store is cleared and the error propagates so the UI can redirect to login.

The useAuthGuard hook tab shows the consumer side. React components simply call authClient and stay oblivious to token mechanics; the hook only reacts when a refresh ultimately fails, listening for a clear event to bounce the user to /login. This separation is the whole point: retry-and-refresh policy is centralized in one interceptor, so feature code reads like ordinary HTTP calls. Watch for pitfalls the code guards against — marking _retry to avoid endless loops, and resetting refreshPromise in a finally so a failed refresh does not permanently wedge the client.


Related snips

Share this code

Here's the card — post it anywhere.

Transparent JWT Refresh With a Single-Flight Axios Response Interceptor — share card
Link copied