Exception Handling#

TVM-FFI gracefully handles exceptions across language boundaries without requiring manual error code management. This document covers throwing, catching, and propagating exceptions in TVM-FFI functions.

Important

Stack traces from all languages are properly preserved and concatenated in the TVM-FFI Stable C ABI.

Cross-language exceptions are first-class citizens in TVM-FFI. TVM-FFI provides a stable C ABI for propagating exceptions across language boundaries, and wraps this ABI to provide language-native exception handling without exposing the underlying C machinery.

Error in C. tvm::ffi::Error is a TVM-FFI object with a TVMFFIErrorCell payload containing:

  • kind: Error type name (e.g., "ValueError", "RuntimeError")

  • message: Human-readable error message

  • backtrace: Stack trace from the point of error

Propagating Errors in C. For call chains, simply propagate return codes—TLS carries the error details:

int my_function(...) {
  int rc = some_ffi_call(...);
  if (rc != 0) return rc;  // Propagate error
  // Continue on success
  return 0;
}

The following sections describe how to throw and catch exceptions across ABI and language boundaries.

Throwing Exceptions#

Python#

Raise native Exception instances or derived classes. TVM-FFI catches these at the ABI boundary and converts them to tvm::ffi::Error objects. When C++ code calls into Python and a Python exception occurs, it propagates back to C++ as a tvm::ffi::Error, which C++ code can handle appropriately.

def my_function(x: int) -> int:
    if x < 0:
        raise ValueError(f"x must be non-negative, got {x}")
    return x + 1

TVM-FFI automatically maps common Python exception types to their corresponding error kinds: RuntimeError, ValueError, TypeError, AttributeError, KeyError, IndexError, AssertionError, and MemoryError. Custom exception types can be registered using tvm_ffi.register_error().

C++#

Use tvm::ffi::Error or the TVM_FFI_THROW macro:

#include <tvm/ffi/error.h>

void ThrowError(int x) {
  if (x < 0) {
    TVM_FFI_THROW(ValueError) << "x must be non-negative, got " << x;
  }
}

The TVM_FFI_THROW macro captures the current file name, line number, stack trace, and error message, then constructs a tvm::ffi::Error object. At the ABI boundary, this error is stored in TLS and the function returns -1 per the TVMFFISafeCallType calling convention (see Calling Convention).

Additional check macros are available for common validation patterns:

TVM_FFI_CHECK(condition, ErrorKind) << "message";  // Custom error kind
TVM_FFI_ICHECK(condition) << "message";            // InternalError
TVM_FFI_ICHECK_EQ(x, y) << "message";              // Check equality
TVM_FFI_ICHECK_LT(x, y) << "message";              // Check less than
// Also: TVM_FFI_ICHECK_GT, TVM_FFI_ICHECK_LE, TVM_FFI_ICHECK_GE, TVM_FFI_ICHECK_NE

Hint

A detailed implementation of such graceful handling behavior can be found in TVM_FFI_SAFE_CALL_BEGIN / TVM_FFI_SAFE_CALL_END macros.

ANSI C#

For LLVM code generation and other C-based environments, use TVMFFIErrorSetRaisedFromCStr() to set the TLS error and return -1:

int Error_RaiseException(void* handle, const TVMFFIAny* args, int32_t num_args, TVMFFIAny* result) {
  TVMFFIErrorSetRaisedFromCStr("ValueError", "Expected at least 2 arguments");
  return -1;
}

For constructing error messages from multiple parts (useful in code generators), use TVMFFIErrorSetRaisedFromCStrParts():

const char* parts[] = {"Expected ", "2", " arguments, got ", "1"};
TVMFFIErrorSetRaisedFromCStrParts("ValueError", parts, 4);
return -1;

Catching Exceptions in C#

In C++, Python, and many other languages, TVM-FFI exceptions are first-class citizens, meaning they can be caught and handled like native exceptions:

try {
   // Calls a TVM-FFI function that throws an exception
} catch (const tvm::ffi::Error& e) {
   // Handle the exception
   std::cout << e.kind() << ": " << e.message() << "\n" << e.backtrace() << "\n";
}

Important

This section covers the pure C low-level details of exception handling. For C++ and other languages, the example above is sufficient.

Checking Return Codes#

When a TVM-FFI function returns a non-zero code, an error occurred. The error object is stored in thread-local storage (TLS) and can be retrieved with TVMFFIErrorMoveFromRaised():

void Error_HandleReturnCode(int rc) {
  TVMFFIObject* err = NULL;
  if (rc == -1) {
    // Move the raised error from TLS (clears TLS slot)
    TVMFFIErrorMoveFromRaised((void**)(&err));  // now `err` owns the error object
    if (err != NULL) {
      PrintError(err);  // print the error
      // IMPORTANT: Release the error object, or gets memory leaks
      TVMFFIObjectDecRef(err);
    }
  }
}

Important

The caller must release the error object via TVMFFIObjectDecRef() to avoid memory leaks.

Accessing Error Details#

The error payload is a TVMFFIErrorCell structure containing the error kind, message, and backtrace. Access it by skipping the TVMFFIObject header:

void PrintError(TVMFFIObject* err) {
  TVMFFIErrorCell* cell = (TVMFFIErrorCell*)((char*)err + sizeof(TVMFFIObject));
  fprintf(stderr, "%.*s: %.*s\n",                        //
          (int)cell->kind.size, cell->kind.data,         // e.g. "ValueError"
          (int)cell->message.size, cell->message.data);  // e.g. "Expected at least 2 arguments"
  if (cell->backtrace.size) {
    fprintf(stderr, "Backtrace:\n%.*s\n", (int)cell->backtrace.size, cell->backtrace.data);
  }
}

Return Code Reference#

Further Reading#