actualbudget/actual · error · Error
token-expired
token-expired
Error message
Authentication failed: invalid or expired session token
What it means
During init(), the previously stored session token is validated via 'subscribe-get-user'. If the user is missing or tokenExpired is true, the stale token is cleared and this error is thrown with code 'token-expired'. It means the server explicitly reported the token as expired or invalid, so the client must re-authenticate.
Source
Thrown at packages/loot-core/src/server/main.ts:304
await sqlite.init();
asyncStorage.init({ persist: false });
await fs.init();
fs._setDocumentDir(dataDir || process.cwd());
if (serverURL) {
setServer(serverURL);
if ('sessionToken' in config && config.sessionToken) {
// Session token authentication
await runHandler(handlers['subscribe-set-token'], {
token: config.sessionToken,
});
// Validate the token
const user = await runHandler(handlers['subscribe-get-user'], undefined);
if (!user || user.tokenExpired === true) {
// Clear invalid token
await runHandler(handlers['subscribe-set-token'], { token: '' });
throw withErrorCode(
new Error('Authentication failed: invalid or expired session token'),
'token-expired',
);
}
if (user.offline === true) {
// Clear token since we can't validate
await runHandler(handlers['subscribe-set-token'], { token: '' });
throw withErrorCode(
new Error('Authentication failed: server offline or unreachable'),
'network-failure',
);
}
} else if ('password' in config && config.password) {
const result = await runHandler(handlers['subscribe-sign-in'], {
password: config.password,
});
if (result?.error) {
// `result.error` is already a machine-readable slug (e.g.View on GitHub (pinned to d4334cb6e6)
Solutions
- Re-authenticate: clear the stored token (the code already clears it) and call init again with the server password
- Verify the client points at the correct sync server URL for the token it holds
- If you run the server, confirm it wasn't reset and its signing secret is stable across restarts
- Log the user in again through the UI to obtain a fresh token
Example fix
// before
await init({ URL: serverUrl, TOKEN: staleToken });
// after
await init({ URL: serverUrl, password }); // password auth re-derives a fresh token Defensive patterns
Strategy: try-catch
Validate before calling
// Check token freshness before init
const token = getStoredToken();
if (!token || isTokenPayloadExpired(token)) {
await reauthenticate(); // get fresh token via password sign-in
} Type guard
function isTokenExpiredError(e: unknown): e is Error & { code: 'token-expired' } {
return e instanceof Error && (e as { code?: string }).code === 'token-expired';
} Try / catch
try {
await init({ URL: serverUrl, TOKEN: storedToken });
} catch (e) {
if (e instanceof Error && (e as { code?: string }).code === 'token-expired') {
await init({ URL: serverUrl, password }); // fall back to password sign-in
} else {
throw e;
}
} Prevention
- Check error.code === 'token-expired' specifically and re-authenticate rather than retrying with the same token
- Fall back to password-based init when the token path fails
- Keep the sync server URL stable; switching servers invalidates tokens
- Monitor server-side token expiry TTL and refresh proactively in long-lived sessions
When it happens
Trigger: Calling init() (directly or via initApp) with SERVER_TOKEN/auth config set to a token the server no longer recognizes: token expired server-side, server restarted with different secret, or the budget/user was deleted.
Common situations: Long-lived desktop session reconnecting after token TTL passed; pointing a client at a different sync server than the one that issued the token; server database reset or user removed.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- responseData.description || responseData.reason || 'unknown'
- unknown
- openid-grant-failed
- User not found
- network-failure
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/67071261d2d921fe.
Report an issue: GitHub.