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
- Balance the parentheses in the pattern source
- Use the error's position/index to locate the stray ')' exactly
- Generate patterns programmatically with a small builder instead of string surgery
- 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
- Lint pattern sources for balanced parens
- Avoid hand-editing pattern strings
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.