yewstack/yew · error
could not remove attribute
Error message
could not remove attribute
What it means
AttributeWriter::remove (packages/yew/src/dom_bundle/btag/attributes.rs:209) calls Element::remove_attribute during diffing when an attribute disappears from a VTag, and unwraps with .expect("could not remove attribute"). remove_attribute throws InvalidCharacterError for names that are not valid attribute names — the same rule as set_attribute. Since removal uses the previously-set name, this panic means an invalid name was stored in the attribute set earlier and only blows up when the attribute is later removed during reconciliation.
Solutions
- Validate attribute names at the boundary where dynamic names are introduced (reject empty strings and space/quote/'<'/'>'/'/'/'=' characters) so they never reach the VTag
- Log attribute keys when inserting into Attributes to catch malformed names at creation time, not at removal time
- Where possible, use static html! attribute names and pass values, not names, dynamically
Example fix
// before
let attrs: Vec<(AttrValue, AttrValue)> = deserialize_from_config();
// after
fn is_valid_attr_name(name: &str) -> bool {
!name.is_empty() && !name.chars().any(|c| matches!(c, ' ' | '"' | '\'' | '<' | '>' | '/' | '=' ) || c.is_control())
}
let attrs: Vec<(AttrValue, AttrValue)> = deserialize_from_config()
.into_iter().filter(|(k, _)| is_valid_attr_name(k)).collect(); Defensive patterns
Strategy: validation
Validate before calling
fn is_valid_attr_name(name: &str) -> bool {
!name.is_empty()
&& !name.chars().any(|c| matches!(c, ' ' | '"' | '\'' | '<' | '>' | '/' | '=') || c.is_control())
}
// filter at ingestion so bad names never enter the attribute set (and never reach removal)
let safe: Vec<_> = incoming_attrs.into_iter().filter(|(k, _)| is_valid_attr_name(k)).collect(); Type guard
fn safe_attr_name(name: &str) -> Option<&str> { is_valid_attr_name(name).then_some(name) } Prevention
- Validate attribute names once, at the point they are created — removal uses the stored name later
- Do not feed raw external/config data into attribute maps without filtering
- Log dropped names in debug builds to spot regressions in name construction
When it happens
Trigger: A dynamically built invalid attribute name was applied earlier (or reached the VTag some other way) and a subsequent render removes that attribute, invoking remove_attribute with the bad name.
Common situations: Spread/rest-props components passing through a malformed key that survives until the element re-renders without it; attribute maps loaded from external data (JSON config, CMS output) containing names with spaces or empty strings; keys assembled by string concatenation with empty segments.
Related errors
- invalid attribute key
- can't create namespaced element for vtag
- could not remove property
- could not set property
- failed to create detached element
AI-assisted analysis of yewstack/yew@0e4a05472f (2026-08-22).
Data as JSON: /api/errors/ecd7f18451c00c3d.
Report an issue: GitHub.
Appendix: source
Thrown at packages/yew/src/dom_bundle/btag/attributes.rs:209
#[cfg(feature = "hydration")]
fn hydrate_set(el: &Element, key: &str, value: &AttributeOrProperty) {
match value {
AttributeOrProperty::Attribute(value) => {
debug_assert_eq!(
el.get_attribute(key).as_deref(),
Some(value.as_ref()),
"attribute `{key}` does not match the server-rendered value during hydration",
);
}
AttributeOrProperty::Property(_) => Self::set(el, key, value),
}
}
fn remove(el: &Element, key: &str, old_value: &AttributeOrProperty) {
match old_value {
AttributeOrProperty::Attribute(_) => el
.remove_attribute(intern(key))
.expect("could not remove attribute"),
AttributeOrProperty::Property(_) => {
let key = JsValue::from_str(key);
js_sys::Reflect::set(el.as_ref(), &key, &JsValue::UNDEFINED)
.expect("could not remove property");
}
}
}
}
impl Apply for Attributes {
type Bundle = Self;
type Element = Element;
fn apply(self, _root: &BSubtree, el: &Element) -> Self {
#[expect(deprecated)]
match &self {
Self::Static(arr) => {
for (k, v) in arr.iter() {View on GitHub (pinned to 0e4a05472f)