Skip to content

[Fix] Surface the real error when environment construction fails, instead of an AttributeError from __del__ - #7373

Draft
hujc7 wants to merge 3 commits into
isaac-sim:developfrom
hujc7:jichuanh/guard-env-del-is-closed
Draft

[Fix] Surface the real error when environment construction fails, instead of an AttributeError from __del__#7373
hujc7 wants to merge 3 commits into
isaac-sim:developfrom
hujc7:jichuanh/guard-env-del-is-closed

Conversation

@hujc7

@hujc7 hujc7 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Description

When environment construction fails partway through __init__, the destructor emits a second,
misleading traceback ahead of the real failure:

Exception ignored in: <function ManagerBasedEnv.__del__ at 0x...>
AttributeError: 'ManagerBasedRLEnv' object has no attribute '_is_closed'

ManagerBasedEnv.__del__ (and the DirectRLEnv / DirectMARLEnv equivalents) read self._is_closed
unconditionally. That attribute is assigned by ManagerBasedEnv.__init__, but a subclass may run
fallible setup before delegating to it
: ManagerBasedRLEnv.__init__ allocates episode_length_buf
and reset_buf on cfg.sim.device before calling super().__init__(). A CUDA OOM, an invalid device,
or a malformed config raises there, leaving _is_closed unset.

The original exception still propagates to the caller — it is not swallowed. But Python cannot raise
out of __del__, so it routes the AttributeError to sys.unraisablehook, which prints it on
unbuffered stderr. The spurious traceback therefore lands first, naming a different exception type
at a different location, which is what makes the real failure hard to find.

This defaults the lookup to True, so an environment that never finished initializing is treated as
already closed and needs no teardown. That matches the existing contract: _is_closed = True is
already the first statement of ManagerBasedEnv.__init__ for exactly this reason, and genuine
partial-init cleanup is handled explicitly by the try/except around _init_sim() that releases the
SimulationContext singleton. Applied to all three env classes, which share an identical destructor.

Why not move the allocation after super().__init__()? Manager construction calls every
observation term once to measure its output shape (ManagerBase.__init__ -> _prepare_terms ->
term_cfg.func(env)), and terms such as mdp.observations.current_time_s read
env.episode_length_buf. The early allocation was introduced deliberately in #2332 for that reason.
Guarding the destructor is the change that does not perturb construction order.

Fixes NVBugs 6604738. Related: 6591313 (AssetBase.__del__) and 6311065 (direct_rl_env.__del__) —
both fixed, neither covers this path; AssetBase uses a different attribute (_is_initialized).

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Release backport

  • Backport this pull request to the active release branch after it merges into develop

Validation

test_env_destructors.py gains one parametrized case covering a partially constructed env. Verified it
fails before the fix and passes after:

Result
Without the fix 3 failedAttributeError at manager_based_env.py:248
With the fix 14 passed (11 existing + 3 new)
uv run --extra test python -m pytest source/isaaclab/test/envs/test_env_destructors.py -q

Known gap

This guards __del__ only. _is_closed is also read unguarded in close() at
manager_based_env.py:605, direct_rl_env.py:591, direct_marl_env.py:593,
manager_based_rl_env.py:358 and leapp_deployment_env.py:427, so
try: env = ManagerBasedRLEnv(cfg) finally: env.close() still raises. A follow-up declaring
_is_closed: bool = True as a class attribute would cover every read site at once.

Checklist

  • I have read and understood the contribution guidelines
  • I have run the pre-commit checks with ./isaaclab.sh --format
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • I have added a changelog fragment under source/<pkg>/changelog.d/ for every touched package
  • I have added my name to the CONTRIBUTORS.md or my name already exists there

The environment destructors read `_is_closed` unconditionally. A subclass may
run fallible setup before delegating to the base `__init__` that assigns the
flag: `ManagerBasedRLEnv` allocates `episode_length_buf` and `reset_buf` before
calling `super().__init__()`, so an allocation failure there leaves the
attribute unset. `__del__` then raises `AttributeError`, which Python reports as
an ignored exception in place of the original construction error, making the
real failure materially harder to diagnose.

Default the lookup to `True` so an environment that never finished initializing
is treated as already closed and needs no teardown.

The buffer allocation cannot simply move after `super().__init__()`: manager
construction calls every observation term once to measure its output shape, and
some terms read `episode_length_buf`.
@github-actions github-actions Bot added the isaac-lab Related to Isaac Lab team label Aug 26, 2026
hujc7 added 2 commits August 27, 2026 16:55
The inline comment restated rationale already carried by the previous commit
message; keep only the fact that forces the `getattr`. The changelog claimed the
destructor masked the original error, but the original exception still
propagates to the caller: the spurious `AttributeError` is printed ahead of it,
not in place of it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant