swc-project/swc · error

You should perform this operation in the closure passed to `

Error message

You should perform this operation in the closure passed to `set` of {}::{}

What it means

Runtime panic from better_scoped_tls (used throughout swc for COMMENTS, hygiene/marks, etc.). `ScopedKey::with` requires the thread-local slot to be set, which only happens inside the closure passed to `KEY.set(&value, ...)`. In debug builds the crate overrides the panic message to name the module and key; in release the underlying scoped_tls still panics with its own message.

Source

Thrown at crates/better_scoped_tls/src/lib.rs:68

    #[cfg_attr(not(debug_assertions), inline(always))]
    pub fn set<F, R>(&'static self, t: &T, f: F) -> R
    where
        F: FnOnce() -> R,
    {
        self.inner.set(t, f)
    }

    /// See [scoped_tls::ScopedKey] for actual documentation.
    #[track_caller]
    #[cfg_attr(not(debug_assertions), inline(always))]
    pub fn with<F, R>(&'static self, f: F) -> R
    where
        F: FnOnce(&T) -> R,
    {
        #[cfg(debug_assertions)]
        if !self.inner.is_set() {
            // Override panic message
            panic!(
                "You should perform this operation in the closure passed to `set` of {}::{}",
                self.module_path, self.name
            )
        }

        self.inner.with(f)
    }

    /// See [scoped_tls::ScopedKey] for actual documentation.
    #[cfg_attr(not(debug_assertions), inline(always))]
    pub fn is_set(&'static self) -> bool {
        self.inner.is_set()
    }
}

#[cfg(test)]
mod tests {

View on GitHub (pinned to 5176682b65)

Solutions

  1. Wrap the entry point in the key's set closure: `COMMENTS.set(&comments, || { ... })`
  2. If the code runs on another thread, call `.set(...)` again inside that thread before using `.with`
  3. Restructure to pass Comments/Handler explicitly as parameters instead of relying on TLS

Example fix

// before
fn my_pass() {
    let c = COMMENTS.with(|c| c.clone()); // panics: not inside set
}

// after
fn my_pass(comments: &Rc<RefCell<Comments>>) {
    COMMENTS.set(comments, || {
        let c = COMMENTS.with(|c| c.clone());
        // ...
    });
}
Defensive patterns

Strategy: validation

Validate before calling

if COMMENTS.is_set() {
    COMMENTS.with(|c| {
        // safe here
    });
} else {
    // not inside a `set` scope: initialize it or return an error
    return Err(anyhow::anyhow!("COMMENTS scope not entered"));
}

Try / catch

let r = std::panic::catch_unwind(|| {
    COMMENTS.with(|c| c.clone())
});
let comments = match r {
    Ok(c) => c,
    Err(_) => return Err(anyhow::anyhow!("TLS key not set: called outside `set` closure")),
};

Prevention

When it happens

Trigger: Calling `COMMENTS.with(|c| ...)` (or any swc TLS accessor) from code that runs outside the corresponding `.set(...)` closure — e.g. invoking a transform pass manually, calling from a freshly spawned thread, or from a drop handler after the set scope ended.

Common situations: Writing custom visitors/plugins that touch comments or hygiene data without entering through the swc compiler entry points; `set` is per-thread, so work moved to another thread loses the scope; debug builds surface it with this exact message.

Related errors


AI-assisted analysis of swc-project/swc@5176682b65 (2026-08-17). Data as JSON: /api/errors/71b630590acc9378. Report an issue: GitHub.