jsonwebtoken throws different errors for different problems — and treating them all as a generic 500 gives users a broken experience. Branch on the error type and add refresh tokens.
The Errors
TokenExpiredError: jwt expired | JsonWebTokenError: invalid signature | JsonWebTokenError: jwt malformed
Branch on the Error Type
const jwt = require("jsonwebtoken");
function verifyToken(req, res, next) {
try {
req.user = jwt.verify(req.token, process.env.JWT_SECRET);
return next();
} catch (err) {
if (err.name === "TokenExpiredError") {
return res.status(401).json({ code: "TOKEN_EXPIRED" });
}
return res.status(401).json({ code: "TOKEN_INVALID" });
}
}Add a Refresh Token Flow
Keep access tokens short-lived (15m) and issue a long-lived refresh token. When the client sees TOKEN_EXPIRED, it silently calls /refresh to get a new access token instead of logging the user out.
const accessToken = jwt.sign({ id }, SECRET, { expiresIn: "15m" });
const refreshToken = jwt.sign({ id }, REFRESH_SECRET, { expiresIn: "30d" });Clock Skew
If tokens 'expire instantly' across servers, their clocks differ. Allow a small tolerance: jwt.verify(token, secret, { clockTolerance: 10 }).
