embassy-rs/embassy · error

InterruptExecutor::start() called multiple times on the…

Error message

InterruptExecutor::start() called multiple times on the same executor.

What it means

`InterruptExecutor::start` uses a `started` Cell guarded by a critical section to guarantee single initialization; calling `start` a second time on the same static executor instance panics because the executor's inner state is already initialized and re-init would corrupt it.

Solutions

  1. Call `start` exactly once per executor instance, e.g. in a dedicated `init()` guarded by `OnceLock`/`once` guard
  2. Use distinct static executors if you genuinely need to start executors at different times or priorities
  3. Guard the start call: check `started` first via `spawner()` succeeding, or wrap start in a `critical_section` check
  4. For multi-core, give each core its own InterruptExecutor static

Example fix

// before
fn init() { EXECUTOR.start(irqs::Irq0); }
init(); init(); // second call panics
// after
use core::sync::atomic::{AtomicBool, Ordering};
static INITED: AtomicBool = AtomicBool::new(false);
fn init() {
    if INITED.swap(true, Ordering::SeqCst) { return; }
    EXECUTOR.start(irqs::Irq0);
}
Defensive patterns

Strategy: validation

Validate before calling

use core::sync::atomic::{AtomicBool, Ordering};
static EXEC_STARTED: AtomicBool = AtomicBool::new(false);
fn start_executor_once(irq: impl InterruptNumber) -> Option<embassy_executor::SendSpawner> {
    if EXEC_STARTED.swap(true, Ordering::SeqCst) { return None; }
    Some(EXECUTOR.start(irq))
}

Prevention

When it happens

Trigger: Calling `executor.start(irq)` twice on the same `static EXECUTOR: InterruptExecutor`, e.g. in both an init function and a later restart path; running the same init code on both cores against a shared static executor; re-running init after a soft reset without re-creating the executor.

Common situations: Refactored boot code that invokes the executor setup from two places; a multi-core app where each core calls the same start function; tests running setup twice in one process with a `#[static_executor]`-style static.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of embassy-rs/embassy@463a07b963 (2026-09-10). Data as JSON: /api/errors/6bec276ef10756ae. Report an issue: GitHub.

Appendix: source

Thrown at embassy-rp/src/executor.rs:230

        /// different "thread" (the interrupt), so spawning tasks on it is effectively
        /// sending them.
        ///
        /// To obtain a [`Spawner`](embassy_executor::Spawner) for this executor, use [`Spawner::for_current_executor()`](embassy_executor::Spawner::for_current_executor()) from
        /// a task running in it.
        ///
        /// # Interrupt requirements
        ///
        /// You must write the interrupt handler yourself, and make it call [`on_interrupt()`](Self::on_interrupt).
        ///
        /// This method already enables (unmasks) the interrupt, you must NOT do it yourself.
        ///
        /// You must set the interrupt priority before calling this method. You MUST NOT
        /// do it after.
        ///
        /// [`SendSpawner`]: embassy_executor::SendSpawner
        pub fn start(&'static self, irq: impl InterruptNumber) -> embassy_executor::SendSpawner {
            if critical_section::with(|cs| self.started.borrow(cs).replace(true)) {
                panic!("InterruptExecutor::start() called multiple times on the same executor.");
            }

            unsafe {
                let context = (irq.number() as usize | (current_core() as usize) << 16) as *mut ();
                (&mut *self.executor.get())
                    .as_mut_ptr()
                    .write(raw::Executor::new(context))
            }

            let executor = unsafe { (&*self.executor.get()).assume_init_ref() };

            unsafe { NVIC::unmask(irq) }

            executor.spawner().make_send()
        }

        /// Get a SendSpawner for this executor
        ///

View on GitHub (pinned to 463a07b963)