babel/babel · error · Error

.visitor["${key}"] may only have .enter and/or .exit handler

Error message

.visitor["${key}"] may only have .enter and/or .exit handlers.

What it means

Thrown by assertVisitorHandler when a per-node visitor object (e.g. visitor.Identifier) contains a property other than 'enter' or 'exit'. The per-node value may be either a single function or an object with exactly the enter/exit keys; any other key is rejected because Babel's traversal has no defined behaviour for it.

Source

Thrown at packages/babel-core/src/config/validation/plugins.ts:67

    if (obj.enter || obj.exit) {
      throw new Error(
        `${msg(
          loc,
        )} cannot contain catch-all "enter" or "exit" handlers. Please target individual nodes.`,
      );
    }
  }
  return obj as Visitor;
}

function assertVisitorHandler(
  key: string,
  value: unknown,
): asserts value is VisitorHandler {
  if (value && typeof value === "object") {
    Object.keys(value).forEach((handler: string) => {
      if (handler !== "enter" && handler !== "exit") {
        throw new Error(
          `.visitor["${key}"] may only have .enter and/or .exit handlers.`,
        );
      }
    });
  } else if (typeof value !== "function") {
    throw new Error(`.visitor["${key}"] must be a function`);
  }
}

type VisitorHandler =
  | Function
  | {
      enter?: Function;
      exit?: Function;
    };

export type PluginObject<S extends PluginPass = PluginPass> = {
  name?: string;

View on GitHub (pinned to 06b6eae39d)

Solutions

  1. Keep only 'enter' and/or 'exit' inside per-node visitor objects.
  2. Store any extra metadata on the plugin's state object (PluginPass) or a closure variable, not on the visitor.
  3. Rename a 'visit' method to 'enter' if that was the intent.

Example fix

// before
visitor: { Identifier: { visit(path){...}, meta: true } }
// after
visitor: { Identifier: { enter(path){...} } }
Defensive patterns

Strategy: type-guard

Validate before calling

function assertVisitorHandlers(visitor) {
  for (const [node, handler] of Object.entries(visitor)) {
    if (handler && typeof handler === 'object') {
      for (const k of Object.keys(handler)) {
        if (k !== 'enter' && k !== 'exit') {
          throw new Error(`visitor.${node} has invalid handler '${k}'`);
        }
      }
    }
  }
}

Type guard

import type { Visitor } from '@babel/traverse';
// Visitor<S> already restricts per-node objects to { enter?, exit? }

Try / catch

try { transformSync(code, { plugins: [plugin] }); } catch (e) {
  if (String(e.message).includes('may only have .enter and/or .exit')) {
    throw new Error('Plugin visitor has an unknown handler key');
  }
  throw e;
}

Prevention

When it happens

Trigger: Writing visitor: { Identifier: { enter(){}, shouldStop: true } } or visitor: { Identifier: { visit(){}, exit(){} } } where 'shouldStop'/'visit' are not valid handler names. The loop at plugins.ts:64-71 throws on the first invalid handler key.

Common situations: Trying to attach metadata to a visitor object; confusing Babel's visitor schema with another library's; using 'visit' instead of 'enter' (Babel uses enter/exit, not visit).

Related errors


AI-assisted analysis of babel/babel@06b6eae39d (2026-08-03). Data as JSON: /data/errors/6d8e1ae98f5004d1.json. Report an issue: GitHub.