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
- Quote the feature as a keyword: use :clj not clj or "clj".
- When calling hasFeature programmatically, pass (keyword f) after validating f is a string/symbol.
- 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
- Always use keywords (:clj :cljs :default) in conditional feature positions
- Convert string/symbol features with (keyword x) before passing to reader APIs
- Validate the :features option set contains only keywords
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
- Feature name is reserved.
- read-cond requires an even number of forms.
- read-cond starting on line
- Unreadable defrecord form: key must be of type…
- arg literal must be %, %& or %integer
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)