mastra-ai/mastra · error
Invalid retention duration: "${duration}". Expected a number
Error message
Invalid retention duration: "${duration}". Expected a number of milliseconds or a "<number><unit>" string (ms, s, m, h, d, w). What it means
parseDuration validates retention duration values for storage (e.g. per-page retention on traces/workflows). It accepts either a number of milliseconds or a string like '30s', '2h', '7d'. If the string doesn't match the pattern ^\d+(?:\.\d+)?(ms|s|m|h|d|w)$, this error is thrown at parse time rather than silently storing an unusable retention config.
Source
Thrown at packages/core/src/storage/utils.ts:71
/**
* Parses a retention {@link Duration} into milliseconds.
*
* Accepts a raw number of milliseconds or a `<number><unit>` string where unit
* is one of `ms`, `s`, `m`, `h`, `d`, `w`.
*
* @throws Error if the input is not a valid duration.
*/
export function parseDuration(duration: Duration): number {
if (typeof duration === 'number') {
if (!Number.isFinite(duration) || duration < 0) {
throw new Error(`Invalid retention duration: ${duration}. Must be a non-negative finite number of milliseconds.`);
}
return duration;
}
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/.exec(duration);
if (!match) {
throw new Error(
`Invalid retention duration: "${duration}". Expected a number of milliseconds or a "<number><unit>" string (ms, s, m, h, d, w).`,
);
}
const value = Number(match[1]);
const unit = match[2]!;
return value * DURATION_UNIT_MS[unit]!;
}
export function safelyParseJSON(input: any): any {
// If already an object (and not null), return as-is
if (input && typeof input === 'object') return input;
if (input == null) return {};
// If it's a string, try to parse
if (typeof input === 'string') {
try {
return JSON.parse(input);
} catch {View on GitHub (pinned to 75dd419e61)
Solutions
- Use one of the supported units exactly: ms, s, m, h, d, w (e.g. '90s', '30m', '7d')
- Pass a plain number of milliseconds instead of a string (e.g. 30000 instead of '30s')
- Trim the string and lowercase it before passing (e.g. ' 30s '.trim().toLowerCase())
- Convert unsupported units yourself: '30min' → '30m', '1day' → '1d'
Example fix
// before
new Mastra({ storage, config: { workflows: { retention: { workflorRuns: '30days' } } } });
// after
new Mastra({ storage, config: { workflows: { retention: { workflowRuns: '30d' } } } }); Defensive patterns
Strategy: validation
Validate before calling
function isValidRetentionDuration(d: unknown): boolean {
if (typeof d === 'number') return Number.isFinite(d) && d >= 0;
return typeof d === 'string' && /^\d+(?:\.\d+)?(ms|s|m|h|d|w)$/.test(d.trim());
}
if (!isValidRetentionDuration(duration)) throw new Error(`Bad duration: ${duration}`); Type guard
function isDurationString(v: unknown): v is `${number}${'ms'|'s'|'m'|'h'|'d'|'w'}` {
return typeof v === 'string' && /^\d+(?:\.\d+)?(ms|s|m|h|d|w)$/.test(v);
} Try / catch
try {
config.retention = parseDuration(userInput);
} catch (e) {
logger.error('Invalid retention duration', { input: userInput, error: e });
config.retention = 7 * 24 * 60 * 60 * 1000; // safe default
} Prevention
- Use a small helper/constants for durations instead of raw strings
- Prefer plain millisecond numbers in programmatic config; reserve strings for human-written config
- Add a startup validation pass that runs parseDuration on all configured durations
- Never build duration strings via templates with free-form unit variables
When it happens
Trigger: Passing a string with an unsupported unit (e.g. '30min', '1day', '24H', '1y'), a bare unitless string ('30'), extra whitespace (' 30s'), a negative value ('-5s'), or a non-numeric string ('weekly') to a retention option.
Common situations: Copy-pasting durations from docs of other libraries that use different unit vocabularies ('min', 'day', 'months'); building durations via template strings like `${n}${'mins'}`; TypeScript typing a config as string and slipping through an invalid value at runtime.
Related errors
- ${name}: id must be provided and cannot be empty.
- Factory storage domain name must not be empty
- AcpAgent does not support resuming suspended generate calls
- ACP prompt stopped before completing: ${response.stopReason}
- MastraAuthBetterAuth needs a database to build its own bette
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/defc12a0b1af7fc4.
Report an issue: GitHub.