Error Handling
Proper error handling is crucial to implement robust serialization as it needs to deal with unknown user input. DataConfig also provide diagnostic to help users quickly pin down common errors such as typos or missing colons in JSON. Here's an example:
# DataConfig Error: Enum name not found in enum type: EDcTestExampleEnum, Actual: 'Far'
- [JsonReader] --> <in-memory>4:25
2 | {
3 | "StrField" : "Lorem ipsum dolor sit amet",
4 | "EnumField" : "Far",
| ^
5 | "Colors" : [
6 | "#FF0000FF", "#00FF00FF", "#0000FFFF"
- [PropertyWriter] Writing property: (FDcTestExampleStruct)$root.(EEDcTestExampleEnum)EnumField
Internally DataConfig applies a consistent error handling strategy across all API. User code is expected to follow along.
Returning FDcResult
The gist is that if a method can fail, it should return a FDcResult
. It's a simple struct:
// DataConfigCore/Public/DataConfig/DcTypes.h
struct DATACONFIGCORE_API DC_NODISCARD FDcResult
{
enum class EStatus : uint8
{
Ok,
Error
};
EStatus Status;
FORCEINLINE bool Ok() const
{
return Status == EStatus::Ok;
}
};
// See FDcReader's methods as example
// DataConfigCore/Public/DataConfig/Reader/DcReader.h
struct DATACONFIGCORE_API FDcReader
{
//...
virtual FDcResult ReadBool(bool* OutPtr);
virtual FDcResult ReadName(FName* OutPtr);
virtual FDcResult ReadString(FString* OutPtr);
virtual FDcResult ReadText(FText* OutPtr);
virtual FDcResult ReadEnum(FDcEnumData* OutPtr);
//...
};
Then use DC_TRY
to call these kinds of functions. The macro itself does early return when result is not Ok
:
// DataConfigCore/Public/DataConfig/DcTypes.h
#define DC_TRY(expr) \
do { \
::FDcResult Ret = (expr); \
if (!Ret.Ok()) { \
return Ret; \
} \
} while (0)
// Example of calling methods returning `FDcResult`
// DataConfigExtra/Private/DataConfig/Extra/Deserialize/DcDeserializeColor.cpp
template<>
FDcResult TemplatedWriteColorDispatch<EDcColorDeserializeMethod::WriterAPI>(const FColor& Color, FDcDeserializeContext& Ctx)
{
DC_TRY(Ctx.Writer->WriteStructRoot(FDcStructStat{ TEXT("Color"), FDcStructStat::WriteCheckName }));
DC_TRY(Ctx.Writer->WriteName(TEXT("B")));
DC_TRY(Ctx.Writer->WriteUInt8(Color.B));
DC_TRY(Ctx.Writer->WriteName(TEXT("G")));
DC_TRY(Ctx.Writer->WriteUInt8(Color.G));
DC_TRY(Ctx.Writer->WriteName(TEXT("R")));
DC_TRY(Ctx.Writer->WriteUInt8(Color.R));
DC_TRY(Ctx.Writer->WriteName(TEXT("A")));
DC_TRY(Ctx.Writer->WriteUInt8(Color.A));
DC_TRY(Ctx.Writer->WriteStructEnd(FDcStructStat{ TEXT("Color"), FDcStructStat::WriteCheckName }));
return DcOk();
}
This pattern is similar to Outcome and std::expected except we give up using the return value. Return values should be passed through reference or pointers in function arguments.
Diagnostics
When implementing a method that returns FDcResult
you have 2 options:
- Return
DcOk()
on succeed. - Return
DC_FAIL(<Catetory>, <ErrId>)
on error.
Examples:
// DataConfigTests/Private/DcTestBlurb.cpp
FDcResult Succeed() {
// succeed
return DcOk();
}
FDcResult Fail() {
// fail !
return DC_FAIL(DcDCommon, Unexpected1) << "My Custom Message";
}
In the examples above DcDCommon
and Unexpected1
are called error category and error id respectively. DcDCommon
is a built-in error category:
// DataConfigCore/Public/DataConfig/Diagnostic/DcDiagnosticCommon.h
namespace DcDCommon
{
static const uint16 Category = 0x1;
enum Type : uint16
{
//...
Unexpected1,
};
} // namespace DcDCommon
// DataConfigCore/Private/DataConfig/Diagnostic/DcDiagnosticCommon.cpp
namespace DcDCommon
{
static FDcDiagnosticDetail _CommonDetails[] = {
// ...
{ Unexpected1, TEXT("Unexpected: '{0}'") },
};
Note that we can pipe argument into the diagnostic. The diagnostic reported by invoking Fail()
would be like:
* # DataConfig Error: Unexpected: 'My Custom Message'
Conclusion
DataConfig uses FDcResult
, DC_TRY
, DC_FAIL
for error handling. It's lightweight and relatively easy to grasp. There's still some limitations though:
FDcResult
occupied the return position making passing value to parent a bit cumbersome.- For now we always stop as the first error. There's no plan to support error recovery.
Some closing notes:
- Reported diagnostics get queued. You'll need to call
FDcEnv::FlushDiags()
to flush them to consumers. - See
DcDiagnosticExtra.h/cpp
for how to register user category. - See
DcEditorExtraModule.cpp - FDcMessageLogDiagnosticConsumer
for custom diagnostic handler and formatting.