Skip to content

Add structured thread-local error state with typed exception classification - #120

Closed
kapellirohith wants to merge 2 commits into
ml-explore:mainfrom
kapellirohith:exception-boundary-fix
Closed

Add structured thread-local error state with typed exception classification#120
kapellirohith wants to merge 2 commits into
ml-explore:mainfrom
kapellirohith:exception-boundary-fix

Conversation

@kapellirohith

@kapellirohith kapellirohith commented Jul 7, 2026

Copy link
Copy Markdown

Motivation

Design proposal discussed in ml-explore/mlx-swift#270 - see the full write-up in this comment.

Language bindings currently receive errors through a process-global push callback (mlx_set_error_handler). A callback has no caller frame to raise a native error from, which forces bindings (e.g. mlx-swift) into out-of-band error collection, and the type of the original C++ exception is discarded - callers cannot distinguish a recoverable OOM from a shape bug. A non-std::exception throw would also unwind across the extern "C" boundary today.

Changes

mlx/c/error.h / error.cpp - structured, thread-local error state (pull model):

  • mlx_error_code enum: INVALID_ARGUMENT / OUT_OF_RANGE / OUT_OF_MEMORY / IO / RUNTIME / UNKNOWN
  • mlx_last_error_code(), mlx_last_error_message(), mlx_clear_last_error() over a thread_local slot
  • _mlx_error_with_code(...) emitter; both emitters still invoke the legacy handler, so existing handler-based callers are unaffected
  • RUNTIME errors are refined to OUT_OF_MEMORY/IO by message inspection ([metal::malloc], [read], [load*, ...) until typed exceptions exist in mlx core

python/c.py - classifying catch ladder in the generator:

  • catches std::invalid_argumentstd::out_of_rangestd::bad_allocstd::exception... (closing the non-std-throw hole)
  • routes e.what() through "%s", fixing a latent printf-format bug when messages contain %
  • second commit applies the ladder mechanically to the checked-in generated sources (507 blocks, byte-identical to generator output); happy to drop that commit if you prefer regenerating via the standard codegen

Compatibility / cost

  • Purely additive API; default behavior (handler → print + exit) unchanged
  • Zero happy-path cost: the ladder and thread_local are only touched on the error path

Verification

Validated end-to-end through a mlx-swift branch that pulls this state into native Swift throws - 6/6 boundary tests pass (broadcast → invalid-argument, max-buffer OOM, corrupt-file IO, error raised on a background thread reaching the caller, no cross-thread slot bleed, first-error propagation). Companion mlx-swift draft PR to follow.

Adds structured, thread-local error state alongside the existing global
handler so language bindings can pull a typed error at the call site
instead of relying on a push callback:

- mlx_error_code enum (invalid_argument / out_of_range / OOM / IO /
  runtime / unknown), classified from the C++ exception type with
  message-based refinement for Metal OOM and load/save failures
- mlx_last_error_code(), mlx_last_error_message(),
  mlx_clear_last_error() over a thread_local slot
- _mlx_error_with_code emitter; both emitters still invoke the legacy
  handler so mlx_set_error_handler users are unaffected
- generator (python/c.py) now emits a classifying catch ladder with a
  trailing catch (...) closing the non-std-throw ABI hole, and routes
  e.what() through "%s" fixing a latent printf-format bug

Generated .cpp files to be refreshed with the standard codegen step
against an MLX checkout.
Mechanical application of the python/c.py CATCH_LADDER template to the
checked-in generated sources (507 catch blocks) so exception types
classify before regeneration against an MLX checkout. Byte-identical to
generator output for these blocks.
@kapellirohith

Copy link
Copy Markdown
Author

This is a prototype supporting the error-handling discussion in ml-explore/mlx-swift#270 — sharing it for feedback on the approach rather than as a finished change.

One note for review: the second commit mechanically applies the classifying catch ladder to the checked-in generated sources (~507 blocks) so the prototype builds. If you'd prefer, I'm happy to drop that commit entirely and let the change be regenerated through the normal codegen step — the only hand-written parts are error.h/error.cpp and the python/c.py template. Also glad to revisit the OOM/IO message-based classification if you'd rather see typed exceptions from mlx core instead.

@andresy

andresy commented Sep 3, 2026

Copy link
Copy Markdown
Member

Closing for now as

i) one can install an error handler with thread-local storage; that is what we currently do for MLX Swift https://github.com/ml-explore/mlx-swift/blob/main/Source/MLX/ErrorHandler.swift#L282

ii) while the error code handling could be of interest, it currently brings to much bloat to the code

@andresy andresy closed this Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants