{"record":{"id":"0550400421a6cddf","repo":"remix-run/remix","slug":"unmatched-055040","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":135,"sourceCode":"      continue\n    }\n\n    if (char === '\\\\') {\n      if (i + 1 === span[1]) {\n        throw new ParseError('dangling escape', source, i)\n      }\n      let text = source.slice(i + 1, i + 2)\n      appendText(text)\n      i += 2\n      continue\n    }\n\n    appendText(char)\n    i += 1\n  }\n  let unmatchedOptional = optionalStack.at(-1)\n  if (unmatchedOptional !== undefined) {\n    throw new ParseError('unmatched (', source, unmatchedOptional.sourceIndex)\n  }\n  if (emptyOptionals.length > 0) {\n    throw new ParseError('empty optional', source, emptyOptionals[0])\n  }\n\n  validateTokenGrammar(source, tokens, optionals, tokenIndices)\n  validateOptionalCaptureSchemas(source, tokens, optionals, tokenIndices)\n\n  return { tokens, optionals, type: options.type }\n}\n\nfunction validateTokenGrammar(\n  source: string,\n  tokens: ReadonlyArray<PartPatternToken>,\n  optionals: ReadonlyMap<number, number>,\n  tokenIndices: ReadonlyArray<number>,\n): void {\n  for (let [index, token] of tokens.entries()) {","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/route-pattern/src/lib/route-pattern/parse.ts#L117-L153","documentation":"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.","triggerScenarios":"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 '('.","commonSituations":"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.","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"],"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-- }\n  return n === 0\n}","typeGuard":"null","tryCatchPattern":"try { parseRoutePattern(source) } catch (e) { if (e instanceof ParseError && e.message === 'unmatched (') { /* close the group at e.position */ } }","preventionTips":["Format patterns so parens align","Generate optionals with paired open/close in one helper"],"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"}