{"record":{"id":"a765f1d0d8661562","repo":"remix-run/remix","slug":"unmatched","errorCode":null,"errorMessage":"unmatched )","messagePattern":"unmatched \\)","errorType":"validation","errorClass":"ParseError","httpStatus":null,"severity":"error","filePath":"packages/route-pattern/src/lib/route-pattern/parse.ts","lineNumber":80,"sourceCode":"\n  let i = span[0]\n  let optionalStack: Array<{ tokenIndex: number; sourceIndex: number }> = []\n  let emptyOptionals: Array<number> = []\n  while (i < span[1]) {\n    let char = source[i]\n\n    if (char === '(') {\n      optionalStack.push({ tokenIndex: tokens.length, sourceIndex: i })\n      tokens.push({ type: char })\n      tokenIndices.push(i)\n      i += 1\n      continue\n    }\n\n    if (char === ')') {\n      let optional = optionalStack.pop()\n      if (optional === undefined) {\n        throw new ParseError('unmatched )', source, i)\n      }\n      if (optional.tokenIndex === tokens.length - 1) {\n        emptyOptionals.push(optional.sourceIndex)\n      }\n      optionals.set(optional.tokenIndex, tokens.length)\n      tokens.push({ type: char })\n      tokenIndices.push(i)\n      i += 1\n      continue\n    }\n\n    if (char === ':') {\n      i += 1\n      let name = IDENTIFIER_RE.exec(source.slice(i, span[1]))?.[0]\n      if (!name) {\n        throw new ParseError('missing variable name', source, i - 1)\n      }\n      tokens.push({ type: ':', name })","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/route-pattern/src/lib/route-pattern/parse.ts#L62-L98","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet pattern = parseRoutePattern('/users(/edit))')\n\n// after\nlet pattern = parseRoutePattern('/users(/edit)')","handlingStrategy":"validation","validationCode":"function isBalanced(s: string) {\n  let n = 0\n  for (let c of s) { if (c === '(') n++; if (c === ')') n--; if (n < 0) return false }\n  return n === 0\n}","typeGuard":"null","tryCatchPattern":"try { parseRoutePattern(source) } catch (e) { if (e instanceof ParseError && e.message === 'unmatched )') { /* use e.position to fix */ } }","preventionTips":["Lint pattern sources for balanced parens","Avoid hand-editing pattern strings"],"tags":["route-pattern","parse","syntax"],"backgroundTag":"unbalanced-parentheses","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}