Error Handling

About

The MAXON API uses a custom error reporting system. In case of an error, a function can return an error object. The return value and the error object are stored in a maxon::Result object. Within a function scope it is also necessary to handle errors returned by sub-functions.

Return Errors

A function that returns a maxon::Result object can either return just the plain return value or a complex error object. This error object can contain detailed information on the error that occurred. For more details on the maxon::Result class see Error Result. For a list of default error types see Error Types.

// This example shows a function that returns a number or an error.
//----------------------------------------------------------------------------------------
// Creates a random number by rolling a dice.
// @return Random number or UnknownError if the dice could not be rolled.
//----------------------------------------------------------------------------------------
static maxon::Result<maxon::Int> GetRandomNumber()
{
maxon::Int number = 0;
if (RollDice(number) == false)
return maxon::UnknownError(MAXON_SOURCE_LOCATION, "Could not roll the dice."_s);
return number;
}

Functions that do not return any value can return Result<void>:

// This example shows a function that does not return a value
// but can return an error in case of an invalid argument or just maxon::OK.
// Note: Real code should use a reference and not a pointer.
//----------------------------------------------------------------------------------------
// Increments the given maxon::Int value.
// Returns an NullptrError if an invalid argument is given.
// @param[in,out] number Pointer to an maxon::Int value.
// @return OK on success.
//----------------------------------------------------------------------------------------
static maxon::Result<void> IncrementValue(maxon::Int* const number)
{
if (number == nullptr)
return maxon::NullptrError(MAXON_SOURCE_LOCATION);
*number = *number + 1;
return maxon::OK;
}

It is possible to create an error object before it is needed to increase the performance in speed critical tasks:

  • MAXON_ERROR_PREALLOCATE: Macro used to preallocate an error object.
// This example uses a preallocated error object to return an error efficiently.
// preallocate the illegal-argument-error g_sampleArgumentError
static MAXON_ERROR_PREALLOCATE(g_sampleArgumentError, []() { return maxon::IllegalArgumentError(MAXON_SOURCE_LOCATION); });
{
// check range
// if out of bounds return the pre-allocated error
if (pos < 0.0 || pos > 1.0)
return g_sampleArgumentError;
return Sample(pos);
}

Error Scope

Within a given error scope one must define how errors returned by sub-functions are handled.

Note
An error returned by a function must be handled explicitly. Ignoring the returned error object will result in a compile error on certain systems.

Best practice to handle errors is typically to declare an error scope and to simply return the error returned by a sub-function:

  • iferr_scope: Declares an error scope.
  • iferr_scope_handler: Executes the defined code when called with iferr_return.
  • iferr_scope_result: Can be used in a finally block to check the error state.

In such an error scope one can simply return from the function if an error occurred in a sub-function:

  • iferr_return: If an error occurred, this error is returned.
//----------------------------------------------------------------------------------------
// Returns a random float value.
// @return A random maxon::Float value.
//----------------------------------------------------------------------------------------
static maxon::Result<maxon::Float> GetRandomFloat()
{
// declare error scope
// return if an error is returned
const maxon::Int randomNumber = GetRandomNumber() iferr_return;
const maxon::Float randomFloat = (maxon::Float)randomNumber / numberOfFaces;
return randomFloat;
}

If a simple error scope is declared with iferr_scope, the attribute iferr_return will return from the function returning the detected error. In contrast, iferr_scope_handler allows to define a function that is called before iferr_return will return.

Note
When called with iferr_return, the return value of iferr_scope_handler is used as the return value of the function.

Compare also Finally.

// This function handles the error returned by GetRandomNumber()
// by printing the error to the console and returning a default value.
//----------------------------------------------------------------------------------------
// Returns a random float value.
// @return A random maxon::Float value.
//----------------------------------------------------------------------------------------
static maxon::Float GetRandomFloat()
{
{
// is called by iferr_return before it returns from the function
DiagnosticOutput("GetRandomFloat() error: @", err);
// defines the return value
return 0.0;
};
// return if an error is returned
const maxon::Int randomNumber = GetRandomNumber() iferr_return;
const maxon::Float randomFloat = (maxon::Float)randomNumber / numberOfFaces;
return randomFloat;
}

It is also possible to simply check if a called function returned an error or not.

  • iferr: Checks if the given function returned an Error. Defines maxon::ErrorPtr err that can be returned.
  • ifnoerr: Checks if the given function returned no error. Defines maxon::NoErrorPtr.
// This function checks if GetRandomNumber() returns an error.
// If so, this error is returned by the function itself.
//----------------------------------------------------------------------------------------
// Returns a random float value.
// @return A random maxon::Float value.
//----------------------------------------------------------------------------------------
static maxon::Result<maxon::Float> GetRandomFloat()
{
maxon::Int randomNumber = 0;
// return if an error is returned
iferr (randomNumber = GetRandomNumber())
{
// "err" is defined in iferr
return err;
}
const maxon::Float randomFloat = (maxon::Float)randomNumber / numberOfFaces;
return randomFloat;
}

If a new error is created within a function it is typically just returned. If one wants iferr_scope_handler to be called before one must use iferr_throw:

  • iferr_throw: Throws an error that is caught in iferr_scope_handler.
// This function prints all errors that occur before the function is left.
// iferr_throw must be used to ensure that this is also true for newly created errors.
static maxon::Float GetRandomFloat(maxon::Float scale)
{
{
// is called by iferr_return or iferr_throw before it returns from the function
DiagnosticOutput("Error: @", err);
// defines the return value
return 0.0;
};
// check "scale" argument
// use iferr_throw to make sure iferr_scope_handler is called
if (scale == 0.0)
iferr_throw(maxon::IllegalArgumentError(MAXON_SOURCE_LOCATION));
// return if an error is returned
const maxon::Int randomNumber = GetRandomNumber() iferr_return;
const maxon::Float randomFloat = (maxon::Float)randomNumber / scale;
return randomFloat;
}

In some cases an error can be ignored. Such cases must be marked and explained explicitly:

Note
Use these attributes only if the error can be ignored or there is no way of handling the error. Always describe the reason why this macro was used.
// This example shows how an error can be ignored explicitly.
// create array and ensure capacity
numbers.EnsureCapacity(count) iferr_return;
// fill array
for (maxon::Int i = 0; i < count; ++i)
{
numbers.Append(i) iferr_cannot_fail("Array capacity ensured");
}
// just try to increase capacity
// use the second parameter "debug" to trigger a debug stop in case of an error
numbers.EnsureCapacity(newCount) iferr_ignore("Capacity size not important.", debug);

Error handling can be limited to a sub-scope within a function by using a lambda:

// This example limits the error handling to an lambda.
// All errors returned from that lambda are handled in one point.
static maxon::Float GetRandomFloat(maxon::Float scale)
{
// define lambda with internal error handling
auto GetFloat = [scale]() -> maxon::Result<maxon::Float>
{
if (scale == 0.0)
return maxon::IllegalArgumentError(MAXON_SOURCE_LOCATION);
// return if an error is returned
const maxon::Int randomNumber = GetRandomNumber() iferr_return;
const maxon::Float randomFloat = (maxon::Float)randomNumber / scale;
return randomFloat;
};
maxon::Float randomFloat = 0.0;
// call lambda and handle error
iferr (randomFloat = GetFloat())
DiagnosticOutput("Error: @", err);
return randomFloat;
}

Error Handling and Resource Management

The attribute iferr_return allows to return from a function when an error was detected. This could lead to issues with resources that have to be managed and freed within the function scope. For example a certain resource must be freed when the function is ending. Allocated objects and memory are best handled using References.

For more complex situations one can use either iferr_scope_handler or finally:

  • The resource clean-up can be handled with iferr_scope_handler. If this is the case any custom error returned from within the function must use iferr_throw to make sure that iferr_scope_handler is called before the function is left.
  • The resource clean-up can be handled with finally.
// This example shows a function that has to prepare and free internal data.
// FreeInternalData() must always be called when the function is left.
// iferr_scope_handler is used to ensure that FreeInternalData() is called
// in the case an error is returned.
static maxon::Result<void> PerformInternalTask()
{
{
// make sure internal data is freed when an error occurred
FreeInternalData();
// print error
DiagnosticOutput("Error: @", err);
// return error
return err;
};
PrepareInternalData() iferr_return;
HandleInternalData() iferr_return;
// check for success
// use iferr_throw to make sure iferr_scope_handler is called
if (GetInternalSuccess() == false)
StoreInternalData() iferr_return;
FreeInternalData();
return maxon::OK;
}
// This example shows a function that has to prepare and free internal data.
// FreeInternalData() must always be called when the function is left.
// The "finally" macro is used to ensure that FreeInternalData() is called
// when the function is left, independent of how the function is left.
static maxon::Result<void> PerformInternalTask()
{
finally
{
// make sure internal data is always freed
FreeInternalData();
};
PrepareInternalData() iferr_return;
HandleInternalData() iferr_return;
// check for success
if (GetInternalSuccess() == false)
return maxon::UnexpectedError(MAXON_SOURCE_LOCATION);
StoreInternalData() iferr_return;
return maxon::OK;
}

Further Reading