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

  1. Use one of the supported units exactly: ms, s, m, h, d, w (e.g. '90s', '30m', '7d')
  2. Pass a plain number of milliseconds instead of a string (e.g. 30000 instead of '30s')
  3. Trim the string and lowercase it before passing (e.g. ' 30s '.trim().toLowerCase())
  4. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/defc12a0b1af7fc4. Report an issue: GitHub.