Unity-Technologies/ml-agents · critical · UnityAgentsException

{m_MethodName} called recursively. This might happen if you

Error message

{m_MethodName} called recursively. This might happen if you call EnvironmentStep() or EndEpisode() from custom code such as CollectObservations() or OnActionReceived().

What it means

RecursionChecker.Start() throws when the wrapped stepping method is entered while it is already running. It guards against re-entrant simulation steps, which corrupt the episode state. Typically caused by user code calling EnvironmentStep() or EndEpisode() from inside callbacks like CollectObservations() or OnActionReceived().

Source

Thrown at com.unity.ml-agents/Runtime/RecursionChecker.cs:19

using System;

namespace Unity.MLAgents
{
    internal class RecursionChecker : IDisposable
    {
        private bool m_IsRunning;
        private string m_MethodName;

        public RecursionChecker(string methodName)
        {
            m_MethodName = methodName;
        }

        public IDisposable Start()
        {
            if (m_IsRunning)
            {
                throw new UnityAgentsException(
                    $"{m_MethodName} called recursively. " +
                    "This might happen if you call EnvironmentStep() or EndEpisode() from custom " +
                    "code such as CollectObservations() or OnActionReceived()."
                );
            }
            m_IsRunning = true;
            return this;
        }

        public void Dispose()
        {
            // Reset the flag when we're done (or if an exception occurred).
            m_IsRunning = false;
        }
    }
}

View on GitHub (pinned to 3ecb446f75)

Solutions

  1. Remove EnvironmentStep()/EndEpisode() calls from CollectObservations(), OnActionReceived(), and sensor code
  2. Set a flag in the callback and act on it in Update() or the next natural step instead of stepping inline
  3. Use Agent.EndEpisode() only from event handlers outside the step pipeline (e.g. FixedUpdate after a trigger check, not during observation collection)
  4. If a reset is needed mid-step, defer it via Agent_Initialize/EpisodeInterrupted or request it through the Agent's built-in reset path

Example fix

// before
public override void CollectObservations(VectorSensor sensor)
{
    if (badState) EndEpisode(); // recursive
}
// after
private bool needsReset;
public override void CollectObservations(VectorSensor sensor)
{
    if (badState) needsReset = true;
}
void FixedUpdate() { if (needsReset) { needsReset = false; EndEpisode(); } }
Defensive patterns

Strategy: try-catch

Validate before calling

// Guard user callbacks: never call EnvironmentStep/EndEpisode inside them
bool inStep = false;
void SafeEndEpisode(Agent agent)
{
    if (!inStep) agent.EndEpisode();
    else agent.EndEpisodeRequested = true; // handle after step completes
}

Try / catch

try
{
    Academy.Instance.EnvironmentStep();
}
catch (UnityAgentsException e) when (e.Message.Contains("called recursively"))
{
    Debug.LogError("EnvironmentStep/EndEpisode was called from inside the step pipeline: " + e.Message);
}

Prevention

When it happens

Trigger: Calling Academy.EnvironmentStep() inside CollectObservations(), OnActionReceived(), or another sensor callback; calling Agent.EndEpisode() from within CollectObservations(); custom editor/debug code invoking EnvironmentStep() from an event fired during the step.

Common situations: Beginners trying to force the environment forward when they need a new observation; resetting an episode mid-observation-collection after detecting a bad state; event-driven designs that step the environment from UI callbacks triggered by simulation events.

Related errors


AI-assisted analysis of Unity-Technologies/ml-agents@3ecb446f75 (2026-09-02). Data as JSON: /api/errors/91d15ac2ffa1c5e7. Report an issue: GitHub.