remix-run/remix · error · ParseError

invalid param delimiter

Error message

invalid param delimiter

What it means

After a named param (`:id`) or wildcard, the next consuming token must act as a delimiter — a separator ('/'), another wildcard, or a dot-prefixed literal (like '.json'). If something else follows (a plain literal glued directly onto the param, e.g. '/users/:idx2' is fine as identifier, but '/users/:id-' style text without a valid delimiter), the boundary between the param and surrounding text is ambiguous and parsePart's grammar validation throws 'invalid param delimiter' at that token.

Source

Thrown at packages/route-pattern/src/lib/route-pattern/parse.ts:174

    for (let next of nextConsumingTokenIndices(tokens, optionals, index + 1)) {
      if (next === tokens.length) continue
      let nextToken = tokens[next]

      if (token.type === '*') {
        if (nextToken.type === '*') {
          throw new ParseError('adjacent wildcards', source, tokenIndices[next])
        }
        continue
      }

      if (
        nextToken.type === 'separator' ||
        nextToken.type === '*' ||
        (nextToken.type === 'text' && nextToken.text.startsWith('.'))
      ) {
        continue
      }
      throw new ParseError('invalid param delimiter', source, tokenIndices[next])
    }
  }
}

function nextConsumingTokenIndices(
  tokens: ReadonlyArray<PartPatternToken>,
  optionals: ReadonlyMap<number, number>,
  start: number,
): Set<number> {
  let result = new Set<number>()
  let pending = [start]
  let visited = new Set<number>()

  while (pending.length > 0) {
    let index = pending.pop()
    if (index === undefined) break
    if (visited.has(index)) continue
    visited.add(index)

View on GitHub (pinned to 9696913134)

Solutions

  1. Separate params with a proper delimiter: '/:a/:b' or use a literal dot '.ext' which is allowed
  2. Restructure '/:lang-:region' into separate segments or a single param you split yourself
  3. Keep extensions in the dot form: '/files/:name(.:ext)' style supported delimiters

Example fix

// before
let pattern = parseRoutePattern('/:lang-:region/posts')

// after
let pattern = parseRoutePattern('/:locale/posts') // split locale yourself, or use separate segments
Defensive patterns

Strategy: validation

Validate before calling

// reject glued params like /:a-:b
if (/:[\w]+[^\w/\s].[\w]*:/.test(source)) { /* review delimiters */ }

Type guard

null

Try / catch

try { parseRoutePattern(source) } catch (e) { if (e instanceof ParseError && e.message === 'invalid param delimiter') { /* restructure segment at e.position */ } }

Prevention

When it happens

Trigger: Calling parseRoutePattern with a pattern where a param is immediately followed by text that is not a legal delimiter, e.g. '/users/:idv2' is one identifier (fine), but '/res/:id@meta' or '/:lang-:region' where '-' glued text follows the param name boundary produces an invalid delimiter token.

Common situations: Trying to embed two params in one segment separated by arbitrary literals ('/:a-:b'); adding file extensions with characters other than a leading dot; localizing patterns with glued separators.

Related errors


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/48f8edf7cd61b9be. Report an issue: GitHub.