Skip to content

Error Handling & Validation Best Practices

Philosophy

  • Fail Fast: Detect errors early in development
  • Graceful Degradation: Handle runtime errors without crashing
  • Clear Messaging: Provide actionable error information
  • Defensive Programming: Validate inputs and state consistently
  • Performance Conscious: Balance safety with performance

UE5 Error Handling Mechanisms

1. Compile-Time Validation

cpp
// Use static_assert for compile-time validation
static_assert(sizeof(FVector) == 12, "FVector size assumption invalid");

// Use UPROPERTY meta tags for editor validation
UPROPERTY(EditAnywhere, meta = (ClampMin = "0.0", ClampMax = "1000.0"))
float Speed = 600.0f;

UPROPERTY(EditAnywhere, meta = (RequiredAssetCheck = "true"))
UInputAction* JumpAction = nullptr;

2. Runtime Validation with ensure()

cpp
// Use ensure() for recoverable errors (development builds only)
void APlatformCharacter::BeginPlay()
{
    Super::BeginPlay();

    // Recoverable - can continue without this asset
    if (!ensure(DefaultMappingContext))
    {
        UE_LOG(LogTemp, Error, TEXT("DefaultMappingContext not set on %s"), *GetName());
        return; // Early return, but don't crash
    }

    // Multiple validations with specific messages
    ensureMsgf(JumpAction, TEXT("JumpAction not configured on %s - jumping disabled"), *GetName());
    ensureMsgf(MoveAction, TEXT("MoveAction not configured on %s - movement disabled"), *GetName());
}

3. Fatal Errors with check()

cpp
// Use check() only for truly unrecoverable errors
void UInteractionComponent::TryInteract()
{
    // This should never happen if properly initialized
    check(GetOwner()); // Will crash if false - use sparingly!

    // Better approach - validate and handle gracefully
    if (!GetOwner())
    {
        UE_LOG(LogInteraction, Fatal, TEXT("InteractionComponent has no owner!"));
        return;
    }
}

4. Logging Levels

cpp
// Define custom log categories
DECLARE_LOG_CATEGORY_EXTERN(LogPlatformer, Log, All);
DECLARE_LOG_CATEGORY_EXTERN(LogInteraction, Log, All);
DECLARE_LOG_CATEGORY_EXTERN(LogAI, Log, All);

// Use appropriate log levels
UE_LOG(LogPlatformer, Fatal, TEXT("Critical error - game cannot continue"));
UE_LOG(LogPlatformer, Error, TEXT("Error occurred but game can continue"));
UE_LOG(LogPlatformer, Warning, TEXT("Unexpected situation but not an error"));
UE_LOG(LogPlatformer, Log, TEXT("Normal operation info"));
UE_LOG(LogPlatformer, Verbose, TEXT("Detailed debug information"));
UE_LOG(LogPlatformer, VeryVerbose, TEXT("Extremely detailed debug info"));

Common Error Patterns & Solutions

1. Null Pointer Access

❌ Dangerous:

cpp
void UMyComponent::UpdateTarget()
{
    // Dangerous - could crash
    TargetActor->SetActorLocation(NewLocation);
    GetOwner()->GetActorLocation(); // Could be null
}

✅ Safe:

cpp
void UMyComponent::UpdateTarget()
{
    // Validate before use
    if (!IsValid(TargetActor))
    {
        UE_LOG(LogPlatformer, Warning, TEXT("TargetActor is invalid"));
        return;
    }

    if (!ensure(GetOwner()))
    {
        UE_LOG(LogPlatformer, Error, TEXT("Component has no owner"));
        return;
    }

    TargetActor->SetActorLocation(NewLocation);
    FVector OwnerLocation = GetOwner()->GetActorLocation();
}

2. Array Bounds Checking

❌ Dangerous:

cpp
void ProcessItems(const TArray<FItem>& Items)
{
    // Could crash if array is empty
    FItem FirstItem = Items[0];

    // Could crash if index is invalid
    FItem SpecificItem = Items[DesiredIndex];
}

✅ Safe:

cpp
void ProcessItems(const TArray<FItem>& Items)
{
    // Check array size before access
    if (Items.Num() == 0)
    {
        UE_LOG(LogPlatformer, Warning, TEXT("Items array is empty"));
        return;
    }

    FItem FirstItem = Items[0];

    // Use IsValidIndex for bounds checking
    if (Items.IsValidIndex(DesiredIndex))
    {
        FItem SpecificItem = Items[DesiredIndex];
    }
    else
    {
        UE_LOG(LogPlatformer, Error, TEXT("Invalid index %d for array of size %d"),
            DesiredIndex, Items.Num());
    }
}

3. Asset Reference Validation

❌ Risky:

cpp
void AMyActor::BeginPlay()
{
    Super::BeginPlay();

    // Assumes asset is always set
    GetAIController()->RunBehaviorTree(BehaviorTreeAsset);
}

✅ Robust:

cpp
void AMyActor::BeginPlay()
{
    Super::BeginPlay();

    // Validate AI controller exists
    AAIController* AIController = GetAIController();
    if (!AIController)
    {
        UE_LOG(LogAI, Warning, TEXT("%s has no AI Controller"), *GetName());
        return;
    }

    // Validate behavior tree asset
    if (!BehaviorTreeAsset)
    {
        UE_LOG(LogAI, Error, TEXT("BehaviorTreeAsset not set on %s"), *GetName());
        return;
    }

    // Only proceed if all validations pass
    AIController->RunBehaviorTree(BehaviorTreeAsset);
    UE_LOG(LogAI, Log, TEXT("Started behavior tree for %s"), *GetName());
}

4. Input Validation

❌ Fragile:

cpp
void UInteractionComponent::SetInteractionRange(float NewRange)
{
    InteractionRange = NewRange; // Could be negative or extremely large
}

✅ Validated:

cpp
void UInteractionComponent::SetInteractionRange(float NewRange)
{
    // Validate input parameters
    if (NewRange < 0.0f)
    {
        UE_LOG(LogInteraction, Warning, TEXT("Negative interaction range not allowed: %.2f"), NewRange);
        return;
    }

    if (NewRange > 10000.0f) // Reasonable maximum
    {
        UE_LOG(LogInteraction, Warning, TEXT("Interaction range too large: %.2f, clamping to 10000"), NewRange);
        NewRange = 10000.0f;
    }

    InteractionRange = NewRange;
    UE_LOG(LogInteraction, VeryVerbose, TEXT("Interaction range set to %.2f"), InteractionRange);
}

Error Handling by System

Character Movement Errors

cpp
void APlatformCharacter::StartSprint()
{
    // Validate movement component
    UCharacterMovementComponent* MovementComp = GetCharacterMovement();
    if (!ensure(MovementComp))
    {
        UE_LOG(LogPlatformer, Error, TEXT("No CharacterMovement component on %s"), *GetName());
        return;
    }

    // Validate we're not already sprinting
    if (bIsSprinting)
    {
        UE_LOG(LogPlatformer, VeryVerbose, TEXT("Already sprinting on %s"), *GetName());
        return;
    }

    // Validate we can sprint (on ground, not disabled, etc.)
    if (!CanSprint())
    {
        UE_LOG(LogPlatformer, Log, TEXT("Cannot sprint: conditions not met for %s"), *GetName());
        return;
    }

    bIsSprinting = true;
    MovementComp->MaxWalkSpeed = SprintSpeed;
    UE_LOG(LogPlatformer, Log, TEXT("Sprint started on %s"), *GetName());
}

bool APlatformCharacter::CanSprint() const
{
    UCharacterMovementComponent* MovementComp = GetCharacterMovement();
    if (!MovementComp)
        return false;

    // Can't sprint if not on ground
    if (!MovementComp->IsMovingOnGround())
        return false;

    // Can't sprint if no input
    if (GetInputAxisValue("Move").IsNearlyZero())
        return false;

    return true;
}

Interaction System Errors

cpp
void UInteractionComponent::TryInteract()
{
    // Validate component state
    if (!IsComponentTickEnabled())
    {
        UE_LOG(LogInteraction, Warning, TEXT("Interaction component disabled on %s"),
            GetOwner() ? *GetOwner()->GetName() : TEXT("Unknown"));
        return;
    }

    // Validate cooldown
    if (!CanInteractNow())
    {
        UE_LOG(LogInteraction, VeryVerbose, TEXT("Interaction on cooldown"));
        return;
    }

    // Validate focus target
    if (!CurrentFocusActor)
    {
        UE_LOG(LogInteraction, Log, TEXT("No interaction target available"));
        return;
    }

    // Validate interface implementation
    if (!CurrentFocusActor->Implements<UInteractable>())
    {
        UE_LOG(LogInteraction, Error, TEXT("Focus actor %s doesn't implement IInteractable"),
            *CurrentFocusActor->GetName());
        CurrentFocusActor = nullptr; // Clear invalid focus
        return;
    }

    // Final validation before interaction
    if (!IInteractable::Execute_CanInteract(CurrentFocusActor, GetOwner()))
    {
        UE_LOG(LogInteraction, Log, TEXT("Cannot interact with %s"), *CurrentFocusActor->GetName());
        return;
    }

    // All validations passed - perform interaction
    LastInteractionTime = GetWorld()->GetTimeSeconds();
    IInteractable::Execute_Interact(CurrentFocusActor, GetOwner());

    UE_LOG(LogInteraction, Log, TEXT("Successfully interacted with %s"), *CurrentFocusActor->GetName());
}

AI System Errors

cpp
void AEnemyNPC::OnPerceptionUpdated(const TArray<AActor*>& UpdatedActors)
{
    // Validate perception component
    UAIPerceptionComponent* PerceptionComp = GetPerceptionComponent();
    if (!ensure(PerceptionComp))
    {
        UE_LOG(LogAI, Error, TEXT("No perception component on %s"), *GetName());
        return;
    }

    // Validate blackboard
    UBlackboardComponent* BlackboardComp = GetBlackboardComponent();
    if (!ensure(BlackboardComp))
    {
        UE_LOG(LogAI, Error, TEXT("No blackboard component on %s"), *GetName());
        return;
    }

    for (AActor* Actor : UpdatedActors)
    {
        if (!IsValid(Actor))
        {
            UE_LOG(LogAI, Warning, TEXT("Invalid actor in perception update"));
            continue;
        }

        // Validate actor is a valid target
        if (!IsValidTarget(Actor))
        {
            continue;
        }

        // Get perception info safely
        FActorPerceptionBlueprintInfo PerceptionInfo;
        if (PerceptionComp->GetActorsPerception(Actor, PerceptionInfo))
        {
            // Process valid perception data
            ProcessTargetPerception(Actor, PerceptionInfo);
        }
    }
}

bool AEnemyNPC::IsValidTarget(AActor* Actor) const
{
    if (!IsValid(Actor))
        return false;

    // Must be a pawn
    APawn* TargetPawn = Cast<APawn>(Actor);
    if (!TargetPawn)
        return false;

    // Must be alive (if has stats component)
    if (UStatsComponent* StatsComp = TargetPawn->FindComponentByClass<UStatsComponent>())
    {
        if (StatsComp->IsDead())
            return false;
    }

    // Must be enemy faction
    return IsEnemy(TargetPawn);
}

Performance Considerations

Minimize Error Checking in Hot Paths

cpp
// In frequently called functions (like Tick), use lighter validation
void UMyComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    // Light validation - only check critical things
    if (UNLIKELY(!GetOwner()))
    {
        // Use UNLIKELY for rare error conditions to help branch prediction
        return;
    }

    // Use checkSlow for debug-only validation
    checkSlow(DeltaTime >= 0.0f);

    // Main logic here...
}

Batch Validation

cpp
bool UMyComponent::ValidateState() const
{
    bool bIsValid = true;

    if (!GetOwner())
    {
        UE_LOG(LogPlatformer, Error, TEXT("No owner"));
        bIsValid = false;
    }

    if (!TargetActor)
    {
        UE_LOG(LogPlatformer, Error, TEXT("No target"));
        bIsValid = false;
    }

    if (SomeImportantValue < 0.0f)
    {
        UE_LOG(LogPlatformer, Error, TEXT("Invalid value: %.2f"), SomeImportantValue);
        bIsValid = false;
    }

    return bIsValid;
}

void UMyComponent::DoImportantOperation()
{
    // Validate everything at once
    if (!ValidateState())
    {
        UE_LOG(LogPlatformer, Error, TEXT("Component state invalid, aborting operation"));
        return;
    }

    // Proceed with operation...
}

Testing Error Conditions

Unit Test Error Handling

cpp
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FCharacterErrorHandlingTest,
    "Platformer.Unit.Character.ErrorHandling",
    EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter)

bool FCharacterErrorHandlingTest::RunTest(const FString& Parameters)
{
    UWorld* World = FPlatformerTestBase::CreateTestWorld();
    APlatformCharacter* Character = FPlatformerTestBase::SpawnTestActor<APlatformCharacter>(World);

    // Test null input handling
    Character->Move(FInputActionValue()); // Empty value
    TestTrue("Character handles null input gracefully", true); // Should not crash

    // Test invalid controller scenario
    Character->SetActorEnableCollision(false);
    Character->PossessedBy(nullptr); // Remove controller
    Character->Move(FInputActionValue(FVector2D(1.0f, 0.0f))); // Should handle gracefully

    return true;
}

Error Recovery Strategies

Graceful Degradation

cpp
void AMyGameMode::HandleCriticalError(const FString& ErrorMessage)
{
    UE_LOG(LogPlatformer, Error, TEXT("Critical error: %s"), *ErrorMessage);

    // Try to recover gracefully
    if (GetWorld())
    {
        // Disable non-essential systems
        DisableNonEssentialSystems();

        // Notify player
        ShowErrorNotification(TEXT("A recoverable error occurred. Some features may be disabled."));

        // Log for analytics
        LogErrorToAnalytics(ErrorMessage);
    }
    else
    {
        // Last resort - controlled shutdown
        RequestEngineExit(TEXT("Unrecoverable error"));
    }
}

Remember: Good error handling makes the difference between a robust, professional game and a fragile prototype.