vitest-dev/vitest · error · Error

Invalid tags expression: unexpected "${formatToken(stream.pe

Error message

Invalid tags expression: unexpected "${formatToken(stream.peek())}" in "${expr}"

What it means

Thrown by parseTagsExpression when a tag filter expression has already been fully parsed but the token stream still has a non-EOF token at the top level. This means the grammar parsed a complete or/and/not expression and then encountered something that cannot legally follow it (e.g. two tags in a row with no operator, or a stray ')'). It signals a malformed --tag / tagsFilter string.

Source

Thrown at packages/vitest/src/runtime/runner/utils/tags.ts:60

    .map(t => `- ${t.name}${t.description ? `: ${t.description}` : ''}`)
    .join('\n')}`)
}

export function createTagsFilter(tagsExpr: string[], availableTags: TestTagDefinition[]): (testTags: string[]) => boolean {
  const matchers = tagsExpr.map(expr => parseTagsExpression(expr, availableTags))
  return (testTags: string[]) => {
    return matchers.every(matcher => matcher(testTags))
  }
}

type TagMatcher = (tags: string[]) => boolean

function parseTagsExpression(expr: string, availableTags: TestTagDefinition[]): TagMatcher {
  const tokens = tokenize(expr)
  const stream = new TokenStream(tokens, expr)
  const ast = parseOrExpression(stream, availableTags)
  if (stream.peek().type !== 'EOF') {
    throw new Error(`Invalid tags expression: unexpected "${formatToken(stream.peek())}" in "${expr}"`)
  }
  return (tags: string[]) => evaluateNode(ast, tags)
}

function formatToken(token: Token): string {
  switch (token.type) {
    case 'TAG': return token.value
    default: return formatTokenType(token.type)
  }
}

type Token
  = | { type: 'TAG'; value: string }
    | { type: 'AND' }
    | { type: 'OR' }
    | { type: 'NOT' }
    | { type: 'LPAREN' }
    | { type: 'RPAREN' }

View on GitHub (pinned to d568f8ce37)

Solutions

  1. Join multiple tags with an explicit operator: use `a && b` or `a and b` instead of `a b`.
  2. Remove the stray token the error points at (e.g. an extra closing paren or duplicate tag).
  3. Re-read the supported grammar: TAG, !NOT, &&/and, ||/or, and (...) grouping.
  4. Test the expression in isolation with the --tag CLI flag to iterate quickly.

Example fix

// before
vitest --tag 'unit smoke'

// after
vitest --tag 'unit && smoke'
Defensive patterns

Strategy: validation

Validate before calling

// Validate a tag expression before passing it to --tag
const TOKEN = /^[!()]+|&&|\|\||\b(?:and|or|not)\b|[^\s!()&|]+/gi
function looksBalanced(expr: string): boolean {
  let depth = 0
  for (const ch of expr) {
    if (ch === '(') depth++
    if (ch === ')') depth--
    if (depth < 0) return false
  }
  return depth === 0
}
if (!looksBalanced(process.env.TAG_FILTER!)) throw new Error('bad tag filter')

Prevention

When it happens

Trigger: Passing a --tag filter with two bare tags separated only by whitespace, e.g. `--tag 'unit smoke'`. Using a closing parenthesis at the top level like `--tag 'unit)'`. Any string where, after a valid or/and/not expression, an unexpected token remains.

Common situations: Writing `--tag 'a b'` instead of `--tag 'a && b'`. Leftover characters after editing a complex filter expression. Misunderstanding that bare juxtaposition is not an implicit AND (you must use && / and).

Related errors


AI-assisted analysis of vitest-dev/vitest@d568f8ce37 (2026-08-03). Data as JSON: /data/errors/683b68d1e649511f.json. Report an issue: GitHub.