{"record":{"id":"7d3985e4683836a2","repo":"remix-run/remix","slug":"empty-optional","errorCode":null,"errorMessage":"empty optional","messagePattern":"empty optional","errorType":"validation","errorClass":"ParseError","httpStatus":null,"severity":"error","filePath":"packages/route-pattern/src/lib/route-pattern/parse.ts","lineNumber":138,"sourceCode":"    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()) {\n    if (token.type !== ':' && token.type !== '*') continue\n\n    for (let next of nextConsumingTokenIndices(tokens, optionals, index + 1)) {","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/route-pattern/src/lib/route-pattern/parse.ts#L120-L156","documentation":"An optional group in the pattern contains no consuming tokens at all — e.g. '()' — making it meaningless and a likely authoring mistake. After parsing, if any optional group was recorded as empty, parsePart throws 'empty optional' at the group's source position.","triggerScenarios":"Calling parseRoutePattern with a source containing '()' or '(())' anywhere in a part, including groups whose only content is another empty optional.","commonSituations":"Template code that wraps an optional segment when the segment string is empty; cleanup scripts stripping params but leaving the parens; copy-paste leftovers.","solutions":["Remove the empty '()' group","If the group was meant to hold an optional param, put the param inside: '(/:page)'","Fix the generator so it omits the whole group when its content is empty"],"exampleFix":"// before\nlet pattern = parseRoutePattern(`/users/:id${opt}`) // opt === ''\n\n// after\nlet pattern = parseRoutePattern(opt ? `/users/:id(${opt})` : '/users/:id')","handlingStrategy":"validation","validationCode":"if (/\\(\\)/.test(source) || /\\(\\(\\)\\)/.test(source)) throw new Error('Empty optional group')","typeGuard":"null","tryCatchPattern":"try { parseRoutePattern(source) } catch (e) { if (e instanceof ParseError && e.message === 'empty optional') { /* remove the () at e.position */ } }","preventionTips":["When templating optionals, omit the whole group when content is empty"],"tags":["route-pattern","parse","syntax","optional-segments"],"backgroundTag":"invalid-route-pattern-syntax","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}