evanw/esbuild · error · LexerPanic

Expected identifier but found %q

Error message

Expected identifier but found %q

What it means

In the named import/export clause parser, esbuild recorded a non-identifier (reserved keyword) where a binding name was expected, and the statement did not turn out to be an 'export ... from' re-export. It then reports 'Expected identifier but found <token>'.

Source

Thrown at internal/js_parser/js_parser.go:5989

			isSingleLine = false
		}
		p.lexer.Next()
		if p.lexer.HasNewlineBefore {
			isSingleLine = false
		}
	}

	if p.lexer.HasNewlineBefore {
		isSingleLine = false
	}
	p.lexer.Expect(js_lexer.TCloseBrace)

	// Throw an error here if we found a keyword earlier and this isn't an
	// "export from" statement after all
	if firstNonIdentifierLoc.Start != 0 && !p.lexer.IsContextualKeyword("from") {
		r := js_lexer.RangeOfIdentifier(p.source, firstNonIdentifierLoc)
		p.log.AddError(&p.tracker, r, fmt.Sprintf("Expected identifier but found %q", p.source.TextForRange(r)))
		panic(js_lexer.LexerPanic{})
	}

	return items, isSingleLine
}

type parseBindingOpts struct {
	isUsingStmt bool
}

func (p *parser) parseBinding(opts parseBindingOpts) js_ast.Binding {
	loc := p.lexer.Loc()

	switch p.lexer.Token {
	case js_lexer.TIdentifier:
		name := p.lexer.Identifier

		// Forbid invalid identifiers
		if (p.fnOrArrowDataParse.await != allowIdent && name.String == "await") ||

View on GitHub (pinned to f6058f8364)

Solutions

  1. Alias reserved words: import { default as d } from 'mod', or use default-import syntax import d from 'mod'.
  2. Correct the keyword to a real identifier.
  3. Re-check generated specifier lists for stray keywords.

Example fix

// before
import { default } from 'mod'
// after
import d from 'mod'
Defensive patterns

Strategy: validation

Validate before calling

import "github.com/evanw/esbuild/pkg/api"
res := api.Transform(src, api.TransformOptions{Loader: api.LoaderJS})
for _, m := range res.Errors {
  if strings.Contains(m.Text, "Expected identifier but found") {
    // a reserved word was used as an import/export binding name
  }
}

Type guard

// Flag reserved words used as bare names in import/export clauses.
var reserved = map[string]bool{"default": true, "class": true, "function": true, "return": true, "var": true, "let": true, "const": true}
var reClauseName = regexp.MustCompile(`(?:import|export)\\s*\\{([^}]*)\\}`)
func importExportUsesReservedWord(src string) bool {
  for _, m := range reClauseName.FindAllStringSubmatch(src, -1) {
    for _, part := range strings.Split(m[1], ",") {
      name := strings.TrimSpace(strings.Fields(part)[0])
      if reserved[name] { return true }
    }
  }
  return false
}

Prevention

When it happens

Trigger: import { default } from 'mod' or export { class } — using a reserved word as a bare specifier name without aliasing, or a stray keyword in the clause list.

Common situations: Using reserved words as import/export names without 'as'; typos; generated code emitting keywords as names.

Related errors


AI-assisted analysis of evanw/esbuild@f6058f8364 (2026-08-09). Data as JSON: /api/errors/95d499134a31a507. Report an issue: GitHub.