remix-run/remix · error · ParseError
unmatched (
Error message
unmatched (
What it means
After scanning a pattern part, an opening '(' for an optional group remains on the stack with no closing ')'. Optional groups must be closed within the same part, so parsePart throws 'unmatched (' positioned at the unclosed paren's source index.
Source
Thrown at packages/route-pattern/src/lib/route-pattern/parse.ts:135
continue
}
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()) {View on GitHub (pinned to 9696913134)
Solutions
- Close every optional group: '/users(/edit)'
- Use the error index to find which '(' is unclosed
- Format/align long patterns so pairs are visually obvious; lint patterns in tests
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-- }
return n === 0
} Type guard
null
Try / catch
try { parseRoutePattern(source) } catch (e) { if (e instanceof ParseError && e.message === 'unmatched (') { /* close the group at e.position */ } } Prevention
- Format patterns so parens align
- Generate optionals with paired open/close in one helper
When it happens
Trigger: Calling parseRoutePattern with a pattern like '/users(/edit' or '/files(/a(/b)' where at least one group never closes. The error points at the innermost/last unclosed '('.
Common situations: Refactoring optional segments and dropping the closer; generators that open groups conditionally but always intend to close; long patterns where the closer was accidentally deleted with a suffix.
Related errors
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/0550400421a6cddf.
Report an issue: GitHub.