Error Handling
Bolt ships with Miraganic Error Handling, a custom plugin for handling errors in blueprint and C++. This plugin has a few goals:
- Make it possible to pass errors between C++ and Blueprint.
- Make defining error types easier in both C++ and Blueprint.
- Pass errors across interfaces. This isn't possible with a static enum type.
- Give feedback to players depending on the error that occurs. (E.g., 'Can't deposit items because the stack is full')
- Allow functions to decide whether they want to handle errors or simply log them.
- Return errors using a lightweight type. Handling errors this way shouldn't affect performance.
Enum Error Handling
Let's use a simple example: dividing. Dividing by zero causes an error. Without the error handling plugin, we might return this error using an enum, like so:
- Blueprint
- C++
enum class EDivisionResult : uint8
{
Success = 0,
// Cannot divide by zero.
DivideByZero
};
EDivisionResult SafeDivide(float& OutResult, float A, float B)
{
if (B == 0.f)
{
return EDivisionResult::DivideByZero;
}
OutResult = A / B;
return EDivisionResult::Success;
}
ECResult
This is simple and works well for this small function, but what if we want to define an interface using this error handling method? We don't know all of the possible errors for our implementors, so it's not possible to define them in an enumeration. This is where the ECResult
type comes in. ECResult
is essentially an 'any enum'; it stores the type of enumeration and the value. In the case above, it would store EDivisionResult
and 0 or 1 depending on the value. Let's look at a function that's meant to be overridden from the plugin:
- Blueprint
- C++
// The function to be overridden
virtual FECResult UBLItemStack::Can_CombineWith(const FBLItemStackView& Other) const;
// Enum for errors emitted by ItemStackA
UENUM()
enum class EItemStackAResult : uint8
{
Success = 0,
ErrorA
};
// A possible implementation
FECResult UBLItemStackA::Can_CombineWith(const FBLItemStackView& Other) const
{
if (!SomeFlag)
{
return EItemStackAResult::ErrorA;
}
// Defaults to success, you can also use FECResult::Success() or EItemStackAResult::Success
return {};
}
// Enum for errors emitted by ItemStackB
UENUM()
enum class EItemStackBResult : uint8
{
Success = 0,
ErrorB
};
// Another possible implementation with different errors
FECResult UBLItemStackB::Can_CombineWith(const FBLItemStackView& Other) const
{
if (!SomeFlag)
{
return EItemStackAResult::ErrorB;
}
// Defaults to success, you can also use FECResult::Success() or EItemStackBResult::Success
return {};
}
ECResult
always treats enum values of 0 as 'Success'.
Returning Results
Both of these implementors can define their own errors and return them. Not only this, defining an error is the same as our previous method using an enum. This is great, but how can we handle the error(s) returned from the function?
- Blueprint
- C++
In Blueprints, you may use the Switch on Result
node. By default, this node always has two pins: Success
and Default
. You may add pins for specific errors that you want to handle. The pins are processed from the top to the bottom; Default
will catch any errors that you haven't explicitly added.
// Note: This function has been simplified for demonstration.
FECResult UBLItemStackA::Can_DepositItems(const FBLItemStackView& Other) const
{
EC_TRY(Can_CombineWith(Other));
// ...
}
Here we use the EC_TRY
macro to pass any errors to the caller. It's the same as this code:
FECResult UBLItemStackA::Can_DepositItems(const FBLItemStackView& Other) const
{
const FECResult CombineResult = Can_CombineWith(Other);
if (CombineResult.IsFailure())
{
return CombineResult;
}
// ...
}
Logging
Most bolt functions do not log results on their own. Rather, they let callers decide whether the results should be logged:
- Blueprint
- C++
UENUM()
enum class EMyError : uint8
{
Success = 0,
// Some error message
SimpleError,
// Formatted error message with parameter: {0}
FormattedError
}
{
const FECResult Result = EMyError::SimpleError;
// To log the result's message:
// (This will log: 'EMyError::SimpleError: Some error message')
EC_LOG_RESULT(LogBolt, Error, Result);
// Log based on a condition, like if it's a failure:
EC_CLOG_RESULT(Result.IsFailure(), LogBolt, Error, Result);
// Log using the result's message as a format:
// (This will log: 'EMyError::FormattedError: Formatted error message with parameter: Some parameter')
EC_LOG_RESULT_FMT(LogBolt, Error, FECResult(EMyError::FormattedError), TEXT("Some parameter"));
}
Errors In Bolt
Bolt functions are typically split into three parts: Can
, Exec
, and Try
. The Can
part checks whether it's
possible to perform the action (e.g., whether depositing an item stack is allowed at all). Can
functions never make
any state changes. Exec
is responsible for actually performing the changes. Try
functions call Can
, then Exec
:
Can
I do this?- Yes ->
Exec
- No -> return the error that occurred.
- Yes ->
If a Try
function fails, it won't change any state.