clojure/clojure · error · RuntimeException

Feature should be a keyword

Error message

Feature should be a keyword: {}

What it means

In reader conditionals (#?), the feature expression must be a keyword such as :clj, :cljs, or :default. hasFeature validates this before checking the feature table and throws if the read feature is any other type. Features are matched against DEFAULT_FEATURE (:default) and the caller-supplied :features option set.

Solutions

  1. Quote the feature as a keyword: use :clj not clj or "clj".
  2. When calling hasFeature programmatically, pass (keyword f) after validating f is a string/symbol.
  3. Validate the feature table contents before reading: every feature in the opts :features set must be a Keyword.

Example fix

// before
(hasFeature "clj" opts) ; string feature
// after
(hasFeature (keyword "clj") opts) ; -> :clj
Defensive patterns

Strategy: type-guard

Validate before calling

(defn feature-keyword? [f] (keyword? f))

Type guard

(defn as-feature [f]
  (cond (keyword? f) f
        (or (symbol? f) (string? f)) (keyword f)
        :else (throw (ex-info "feature must be keyword-able" {:feature f}))))

Try / catch

(try (hasFeature f opts) (catch RuntimeException e :unsupported-feature))

Prevention

When it happens

Trigger: Evaluating (reader-conditional/read with cond) where a feature form reads as a non-keyword, e.g. (clj/reader-conditional ...) misuse or a form like #?(:"clj" ...) or feature position holding a symbol/number.

Common situations: Writing custom reader-conditional evaluation with features passed as strings instead of keywords; typo'd or generated conditional forms where the feature slot contains a symbol like clj instead of :clj.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of clojure/clojure@f3b143341d (2026-09-09). Data as JSON: /api/errors/9f0cba1b12072a45. Report an issue: GitHub.

Appendix: source

Thrown at src/jvm/clojure/lang/LispReader.java:1526

	if(RT.booleanCast(READ_COND_ENV.deref()) && opts instanceof IPersistentMap)
    {
        Object readCond = ((IPersistentMap) opts).valAt(OPT_READ_COND);
        return COND_PRESERVE.equals(readCond);
    }
    else
        return false;
}

public static class ConditionalReader extends AFn {

	final static private Object READ_STARTED = new Object();
	final static public Keyword DEFAULT_FEATURE = Keyword.intern(null, "default");
	final static public IPersistentSet RESERVED_FEATURES =
		RT.set(Keyword.intern(null, "else"), Keyword.intern(null, "none"));

	public static boolean hasFeature(Object feature, Object opts) {
		if (! (feature instanceof Keyword))
			throw Util.runtimeException("Feature should be a keyword: " + feature);

		if(DEFAULT_FEATURE.equals(feature))
			return true;

		IPersistentSet custom = (IPersistentSet) ((IPersistentMap)opts).valAt(OPT_FEATURES);
		return custom != null && custom.contains(feature);
	}

	public static Object readCondDelimited(PushbackReader r, boolean splicing, Object opts, Object pendingForms) {
		Object result = READ_STARTED;
		Object form; // The most recently ready form
		boolean toplevel = (pendingForms == null);
		pendingForms = ensurePending(pendingForms);

		final int firstline =
				(r instanceof LineNumberingPushbackReader) ?
						((LineNumberingPushbackReader) r).getLineNumber() : -1;

View on GitHub (pinned to f3b143341d)