Error handling and MonoError
MonoError
MonoError is the latest attempt at cleaning up and sanitizing error handling in the runtime. This document highlights some of the design goals and decisions, the implementation and the migration strategy.
Design goals
-
Replace the majority of the adhoc error handling subsystems present today in the runtime. Each one is broken in a subtle way, has slightly different semantics and error conversion between them is spot, at best.
-
Map well to the final destination of all runtime errors: managed exceptions. This includes being compatible with .net when it comes to the kind of exception produced by a given error condition.
-
Be explicit, lack any magic. The loader-error setup does control flow happens in the background through a TLS variable, which made it very brittle and error prone.
-
Explicit and multiple error scopes. Make it possible to have multiple error scopes and make them explicit. We need to support nested scopes during type loading, even if reporting is flat.
-
Be as simple as possible. Error handling is the hardest part of the runtime to test so it must be simple. Which means complex error reporting, such as chaining, is out of question.
Current implementation
The current implementation exists in mono-error.h and mono-error-internals.h. The split is so API users can consume errors, but they are not supported to be able to produce them - such use case has yet to arise.
Writing a function that produces errors
/**
*
* @returns NULL on error
*/
void*
my_function (int a, MonoError *error)
{
if (a <= 0) {//
mono_error_set_argument (error, "a", "argument a must be bigger than zero, it was %d", a);
return NULL;
}
return malloc (a);
}
Important points from the above:
- Add a “MonoError *error” argument as the last to your function
- Call one of the mono_error_set functions based on what managed exception this should produce and the available information
- Document that a NULL returns means an error
Writing a function that consumes errors
void
other_function (void)
{
ERROR_DECL (error);
void *res;
res = my_function (10, error);
//handling the error:
//1st option: set the pending exception. Only safe to do in icalls
if (mono_error_set_pending_exception (error)) //returns TRUE if an exception was set
return;
//2nd option: legacy code that can't handle failures:
mono_error_assert_ok (error);
//3rd option (deprecated): raise an exception and write a FIXME note
// (implicit cleanup, no-op if there was no error)
mono_error_raise_exception (error); /* FIXME don't raise here */
//4th option: ignore
mono_error_cleanup (error);
}
Important points from the above:
- Use
ERROR_DECL (error)
to declare and initialize aMonoError *error
variable. (Under the hood, it declares a localMonoError error_value
usingERROR_DECL_VALUE (error_value)
. You may useERROR_DECL_VALUE (e)
to declare a variable local variable yourself. It’s pretty unusual to need to do that, however.) - Pass it to the required function and always do something with the result
- Given we’re still transitioning, not all code can handle in the same ways
Handling the transition
The transition work is not complete and we’re doing it piece-by-piece to ensure we don’t introduce massive regressions in the runtime. The idea is to move the least amount of code a time to use the new error machinery.
Here are the rules for code conversion:
-
Mono API functions that need to call functions which take a MonoError should assert on failure or cleanup the error as there’s no adequate alternative at this point. They must not use
mono_error_raise_exception
ormono_error_set_pending_exception
-
When possible, change the function signature. If not, add a _checked variant and add the
MONO_RT_EXTERNAL_ONLY
to the non-checked version if it’s in the Mono API. That symbol will prevent the rest of the Mono runtime from calling the non-checked version.
Advanced technique: using a local error to raise a different exception
Suppose you want to call a function foo_checked()
but you want to raise a different exception if it fails. In this case, it makes sense to create a local error variable to handle the call to foo_checked
:
int
my_function (MonoObject *arg, MonoError *error)
{
ERROR_DECL (local_error);
int result = foo_checked (arg, local_error);
if (!is_ok (local_error)) {
mono_error_set_execution_engine (error, "Could not successfully call foo_checked, due to: %s", mono_error_get_message (local_error));
mono_error_cleanup (local_error);
}
return result;
- Pass
local_error
tofoo_checked
- Check the result and if it wasn’t okay, set a different error code on
error
It is common to usemono_error_get_message
to include the message from the local failure as part of the new exception - Cleanup
local_error
to release its resources
Advanced technique: MonoErrorBoxed and mono_class_set_failure
Normally we store a MonoError
on the stack. The usual scenario is that managed code calls into the runtime, we perform some operations, and then we either return a result or convert a MonoError
into a pending exception. So a stack lifetime for a MonoError
makes sense.
There is one scenario where we need a heap-allocated MonoError
whose lifetime is tied to a MonoImage
: the initialization of a managed class. MonoErrorBoxed
is a thin wrapper around a MonoError
that identifies a MonoError
that is allocated in the mempool of a MonoImage
. It is created using mono_error_box()
and converted back to an ordinary MonoError
using mono_error_unbox()
.
static int
some_class_init_helper (MonoClass *k)
{
if (mono_class_has_failure (k))
return -1; /* Already a failure, don't bother trying to init it */
ERROR_DECL (local_error);
int result = foo_checked (k, local_error);
if (!is_ok (error)) {
mono_class_set_failure (k, mono_error_box (local_error, k->image));
mono_error_cleanup (local_error);
}
return result;
}
- Check whether the class is already marked as a failure
- Pass a
local_error
tofoo_checked
- Check the result and if it wasn’t okay, allocate a boxed
MonoError
in the mempool of the class’s image - Mark the class that failed with the boxed error
- Cleanup the
local_error
to release its resources
Design issues
- Memory management of the error setting functions is not consistent or clear
- Use a static initializer in the declaration site instead of mono_error_init?
- Force an error to always be set or only when there’s an exception situation? I.E. mono_class_from_name failing to find the class X finding the class but it failed to load.
- g_assert (mono_errork_ok (&error)) could be replaced by a macro that uses g_error so we can see the error contents on crashes.