bevyengine/bevy · critical

Draw function {} not found for {}

Error message

Draw function {} not found for {}

What it means

DrawFunctions<P>::id::<T>() (draw.rs:99) panics with 'Draw function {T} not found for {P}' when no Draw function of type T has been registered for the phase item type P. It is the panicking counterpart of get_id::<T>(), which returns Option, and its doc explicitly says it panics if the id doesn't exist.

Source

Thrown at crates/bevy_render/src/render_phase/draw.rs:101

    /// Retrieves the [`Draw`] function corresponding to the `id` mutably.
    pub fn get_mut(&mut self, id: DrawFunctionId) -> Option<&mut dyn Draw<P>> {
        self.draw_functions.get_mut(id.0 as usize).map(|f| &mut **f)
    }

    /// Retrieves the id of the [`Draw`] function corresponding to their associated type `T`.
    pub fn get_id<T: 'static>(&self) -> Option<DrawFunctionId> {
        self.indices.get(&TypeId::of::<T>()).copied()
    }

    /// Retrieves the id of the [`Draw`] function corresponding to their associated type `T`.
    ///
    /// Fallible wrapper for [`Self::get_id()`]
    ///
    /// ## Panics
    /// If the id doesn't exist, this function will panic.
    pub fn id<T: 'static>(&self) -> DrawFunctionId {
        self.get_id::<T>().unwrap_or_else(|| {
            panic!(
                "Draw function {} not found for {}",
                core::any::type_name::<T>(),
                core::any::type_name::<P>()
            )
        })
    }
}

/// Stores all draw functions for the [`PhaseItem`] type hidden behind a reader-writer lock.
///
/// To access them the [`DrawFunctions::read`] and [`DrawFunctions::write`] methods are used.
#[derive(Resource)]
pub struct DrawFunctions<P: PhaseItem> {
    internal: RwLock<DrawFunctionsInternal<P>>,
}

impl<P: PhaseItem> Default for DrawFunctions<P> {
    fn default() -> Self {

View on GitHub (pinned to 396ca72708)

Solutions

  1. Register the draw function before first use: app.add_render_command::<CustomPhaseItem, DrawCustom>() in your plugin's build().
  2. Use get_id::<T>() and handle None gracefully instead of the panicking id().
  3. Ensure the plugin that initializes DrawFunctions<P> and registers commands is added before the code that requests ids (check plugin ordering).

Example fix

// before
let draw_id = draw_functions.read().id::<DrawCustomPhaseItemCommands>(); // panics if unregistered

// after
let draw_functions = draw_functions.read();
let Some(draw_id) = draw_functions.get_id::<DrawCustomPhaseItemCommands>() else {
    error!("DrawCustomPhaseItemCommands not registered for this phase yet");
    return;
};
Defensive patterns

Strategy: validation

Validate before calling

let draw_functions = draw_functions.read();
if draw_functions.get_id::<DrawCustom>().is_none() {
    // register before first use instead of letting id() panic
    error!("DrawCustom not registered for this phase");
    return;
}

Prevention

When it happens

Trigger: Calling draw_functions.read().id::<T>() where T (typically a RenderCommandState wrapping your RenderCommand) was never added via app.add_render_command::<P, T>() or DrawFunctions::write().add(..) before the call.

Common situations: Custom PhaseItems: a plugin queues items using a DrawFunctionId from id::<DrawX>() but forgot app.add_render_command::<CustomPhaseItem, DrawX>(); initialization order where ids are requested before registration; refactors that changed the command type so registered and requested types differ.

Related errors


AI-assisted analysis of bevyengine/bevy@396ca72708 (2026-08-20). Data as JSON: /api/errors/fff5630a60593b45. Report an issue: GitHub.