remix-run/remix · error · ParseError

unmatched )

Error message

unmatched )

What it means

While tokenizing a pattern part, a closing parenthesis ')' was found with no matching opening '(' on the optional-group stack. Optional segments must be balanced, so parsePart throws a ParseError 'unmatched )' positioned at the offending character.

Source

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

  let i = span[0]
  let optionalStack: Array<{ tokenIndex: number; sourceIndex: number }> = []
  let emptyOptionals: Array<number> = []
  while (i < span[1]) {
    let char = source[i]

    if (char === '(') {
      optionalStack.push({ tokenIndex: tokens.length, sourceIndex: i })
      tokens.push({ type: char })
      tokenIndices.push(i)
      i += 1
      continue
    }

    if (char === ')') {
      let optional = optionalStack.pop()
      if (optional === undefined) {
        throw new ParseError('unmatched )', source, i)
      }
      if (optional.tokenIndex === tokens.length - 1) {
        emptyOptionals.push(optional.sourceIndex)
      }
      optionals.set(optional.tokenIndex, tokens.length)
      tokens.push({ type: char })
      tokenIndices.push(i)
      i += 1
      continue
    }

    if (char === ':') {
      i += 1
      let name = IDENTIFIER_RE.exec(source.slice(i, span[1]))?.[0]
      if (!name) {
        throw new ParseError('missing variable name', source, i - 1)
      }
      tokens.push({ type: ':', name })

View on GitHub (pinned to 9696913134)

Solutions

  1. Balance the parentheses in the pattern source
  2. Use the error's position/index to locate the stray ')' exactly
  3. Generate patterns programmatically with a small builder instead of string surgery
  4. Add parse tests for every registered route pattern

Example fix

// before
let pattern = parseRoutePattern('/users(/edit))')

// after
let pattern = parseRoutePattern('/users(/edit)')
Defensive patterns

Strategy: validation

Validate before calling

function isBalanced(s: string) {
  let n = 0
  for (let c of s) { if (c === '(') n++; if (c === ')') n--; if (n < 0) return false }
  return n === 0
}

Type guard

null

Try / catch

try { parseRoutePattern(source) } catch (e) { if (e instanceof ParseError && e.message === 'unmatched )') { /* use e.position to fix */ } }

Prevention

When it happens

Trigger: Calling parseRoutePattern with a source containing an extra ')', e.g. '/users(/edit))' or '/a)b/c'. Position in the error points at the stray paren.

Common situations: Hand-editing pattern strings and deleting an opening paren; code-generating optionals with broken conditionals; copy-paste from docs where a wrapping paren got duplicated.

Related errors


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