remix-run/remix · error · ParseError

empty optional

Error message

empty optional

What it means

An optional group in the pattern contains no consuming tokens at all — e.g. '()' — making it meaningless and a likely authoring mistake. After parsing, if any optional group was recorded as empty, parsePart throws 'empty optional' at the group's source position.

Source

Thrown at packages/route-pattern/src/lib/route-pattern/parse.ts:138

    if (char === '\\') {
      if (i + 1 === span[1]) {
        throw new ParseError('dangling escape', source, i)
      }
      let text = source.slice(i + 1, i + 2)
      appendText(text)
      i += 2
      continue
    }

    appendText(char)
    i += 1
  }
  let unmatchedOptional = optionalStack.at(-1)
  if (unmatchedOptional !== undefined) {
    throw new ParseError('unmatched (', source, unmatchedOptional.sourceIndex)
  }
  if (emptyOptionals.length > 0) {
    throw new ParseError('empty optional', source, emptyOptionals[0])
  }

  validateTokenGrammar(source, tokens, optionals, tokenIndices)
  validateOptionalCaptureSchemas(source, tokens, optionals, tokenIndices)

  return { tokens, optionals, type: options.type }
}

function validateTokenGrammar(
  source: string,
  tokens: ReadonlyArray<PartPatternToken>,
  optionals: ReadonlyMap<number, number>,
  tokenIndices: ReadonlyArray<number>,
): void {
  for (let [index, token] of tokens.entries()) {
    if (token.type !== ':' && token.type !== '*') continue

    for (let next of nextConsumingTokenIndices(tokens, optionals, index + 1)) {

View on GitHub (pinned to 9696913134)

Solutions

  1. Remove the empty '()' group
  2. If the group was meant to hold an optional param, put the param inside: '(/:page)'
  3. Fix the generator so it omits the whole group when its content is empty

Example fix

// before
let pattern = parseRoutePattern(`/users/:id${opt}`) // opt === ''

// after
let pattern = parseRoutePattern(opt ? `/users/:id(${opt})` : '/users/:id')
Defensive patterns

Strategy: validation

Validate before calling

if (/\(\)/.test(source) || /\(\(\)\)/.test(source)) throw new Error('Empty optional group')

Type guard

null

Try / catch

try { parseRoutePattern(source) } catch (e) { if (e instanceof ParseError && e.message === 'empty optional') { /* remove the () at e.position */ } }

Prevention

When it happens

Trigger: Calling parseRoutePattern with a source containing '()' or '(())' anywhere in a part, including groups whose only content is another empty optional.

Common situations: Template code that wraps an optional segment when the segment string is empty; cleanup scripts stripping params but leaving the parens; copy-paste leftovers.

Related errors


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/7d3985e4683836a2. Report an issue: GitHub.