diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 59a3fece04..5071144691 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -289,6 +289,12 @@ class Scheduler { SchedulerItem::Type type, bool match_retry) { size_t count = 0; for (auto &item : container) { + // Skip nullptr items (can happen in defer_queue_ when items are being processed) + // The defer_queue_ uses index-based processing: items are std::moved out but left in the + // vector as nullptr until cleanup. If cancel_item_locked_() is called from a callback during + // defer queue processing, it will iterate over these nullptr items. This check prevents crashes. + if (!item) + continue; if (this->matches_item_(item, component, name_cstr, type, match_retry)) { // Mark item for removal (platform-specific) #ifdef ESPHOME_THREAD_MULTI_ATOMICS @@ -311,6 +317,12 @@ class Scheduler { bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, bool match_retry) const { for (const auto &item : container) { + // Skip nullptr items (can happen in defer_queue_ when items are being processed) + // The defer_queue_ uses index-based processing: items are std::moved out but left in the + // vector as nullptr until cleanup. If this function is called during defer queue processing, + // it will iterate over these nullptr items. This check prevents crashes. + if (!item) + continue; if (is_item_removed_(item.get()) && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, /* skip_removed= */ false)) {