SuperAPI AI Directory

logging-and-assertions

Coding & development

View repository

Updated 11-06 16:23

About this skill

Logging & assertions

Good diagnostics make UE bugs findable in minutes rather than hours. Use log categories (not LogTemp), pick the right verbosity so logs stay scannable, and choose check vs ensure deliberately — one halts, the other reports and continues.

When to use this skill

  • Defining a log category for a new game module or feature subsystem.
  • Adding UE_LOG / UE_LOGFMT calls with the right verbosity for the context.
  • Choosing between check, verify, and ensure for a given assumption.
  • Deciding whether a bad state should crash the program or just report.
  • Filtering or silencing verbose logs per-category at runtime or in builds.

Log categories

Every UE_LOG call names a category. Use the built-in LogTemp for quick prototyping; give each module its own category in production so Output Log filtering works.

Declare in a header, define once in a .cpp:

// MyFeature.h  (or a dedicated Logging.h for the module)
DECLARE_LOG_CATEGORY_EXTERN(LogMyFeature, Log, All);
//                                         ^--- default runtime verbosity
//                                               ^--- compile-time maximum (All = keep everything)

// MyFeature.cpp
DEFINE_LOG_CATEGORY(LogMyFeature);

DECLARE_LOG_CATEGORY_EXTERN generates an extern struct; DEFINE_LOG_CATEGORY provides the definition. A file-private category uses DEFINE_LOG_CATEGORY_STATIC and needs no header declaration — appropriate for a single .cpp implementation file.

The compile-time maximum (All above) determines which verbosity levels are compiled in. Setting it to Warning strips Log/Verbose/VeryVerbose calls at compile time in that category, even in debug builds. All is the standard choice; tighten only for hot-path code.

See references/log-categories-and-verbosity.md for the full verbosity table, runtime console commands, command-line flags, and DEFINE_LOG_CATEGORY_STATIC vs class-static patterns.

UE_LOG — formatted logging

// Basic usage: category, verbosity, format string (must be TEXT()), then args
UE_LOG(LogMyFeature, Log,     TEXT("Spawned %s at %s"), *Actor->GetName(), *Loc.ToString());
UE_LOG(LogMyFeature, Warning, TEXT("Ammo low: remaining=%d"), Ammo);
UE_LOG(LogMyFeature, Error,   TEXT("Null weapon on %s — skipping fire"), *GetName());

Format specifiers: %s expects a const TCHAR* — dereference FString with * to get one. %d int32, %f float/double, %p pointer. Using an FString directly (without *) as %s is the most common logging bug.

UE_CLOG(Condition, Category, Verbosity, Format, ...) is the conditional variant — the format is only evaluated when Condition is true.

UE_LOGFMT — structured logging (UE 5.2+)

UE_LOGFMT records a structured log event with named fields, enabling machine-readable output and richer tooling. Requires #include "Logging/StructuredLog.h".

#include "Logging/StructuredLog.h"

// Positional: values map left-to-right to {field} placeholders in format
UE_LOGFMT(LogMyFeature, Warning, "Weapon '{Name}' fired with {Ammo} rounds left",
    *WeaponName, AmmoCount);

// Named: order is irrelevant; extra fields are allowed
UE_LOGFMT(LogMyFeature, Warning, "Weapon '{Name}' fired with {Ammo} rounds left",
    ("Name", *WeaponName), ("Ammo", AmmoCount), ("ActorTag", *GetName()));

Field names must match [A-Za-z0-9_]+ and be unique within the call. Do not mix positional and named styles in the same call. Supports up to 16 fields via UE_LOGFMT; use UE_LOGFMT_EX with UE_LOGFMT_FIELD("Name", Value) wrappers for more.

See references/structured-logging.md for the field serialization protocol, UE_LOG_CONTEXT, FLogRecord, and thread-local context scopes.

Verbosity levels

From most to least severe (verbosity Fatal = highest priority):

Level Output Typical use
Fatal Crashes immediately, always, even with NO_LOGGING Truly unrecoverable error
Error Console + log file, highlighted red Wrong state that will cause visible malfunction
Warning Console + log file, highlighted yellow Recoverable but unexpected; code worked around it
Display Console + log file Infrequent informational events (startup, asset loads)
Log Log file only (not console) Normal operational info; may be noisy
Verbose Log file if category enabled Detailed trace, disabled by default
VeryVerbose Log file if category enabled Per-frame or very-high-frequency trace

Runtime adjustment without restarting: Log LogMyFeature Verbose (console) or -LogCmds="LogMyFeature Verbose" (command line). The runtime level cannot exceed the compile-time maximum baked into the category.

Assertions

Three families — choose based on whether the expression must run and what happens on failure:

Macro Dev behavior Shipping Expression runs in shipping?
check(expr) halts + callstack compiled out no
checkf(expr, fmt, ...) halts + message + callstack compiled out no
checkSlow(expr) halts in Debug only compiled out no
checkNoEntry() halts if hit compiled out —
verify(expr) halts if false expression runs, no halt yes
verifyf(expr, fmt, ...) halts + message expression runs, no halt yes
ensure(expr) logs + callstack once, continues expression runs, no report yes
ensureMsgf(expr, fmt, ...) logs + message + callstack once, continues expression runs yes
ensureAlways(expr) logs + callstack every time expression runs yes
// check: hard invariant — continuing is unsafe if false
check(Weapon != nullptr);
checkf(Index >= 0 && Index < Count, TEXT("Index %d out of range [0, %d)"), Index, Count);

// verify: side-effecting call that must both run and succeed
verify((Mesh = GetRenderMesh()) != nullptr);  // GetRenderMesh() runs even in shipping

// ensure: unexpected-but-recoverable; keeps editor alive, submits crash report once
if (!ensure(Target != nullptr)) { return; }
ensureMsgf(bInitialized, TEXT("%s used before Init()"), *GetName());

// checkNoEntry: unreachable code sentinel
default: checkNoEntry(); break;

Rule of thumb for gameplay code: prefer ensure over check. An editor that continues running lets you investigate the bad state in the Output Log and debugger; a crash destroys that context. Reserve check for truly invariant conditions where execution past the failure would corrupt state or crash anyway.

See references/assertions.md for compile-flag mechanics (DO_CHECK, DO_ENSURE, USE_CHECKS_IN_SHIPPING), FDebug helpers, and LowLevelFatalError.

On-screen debug messages (PIE only)

if (GEngine)
{
    GEngine->AddOnScreenDebugMessage(
        -1,              // key: -1 = no deduplication; use a stable int to overwrite the same slot
        5.f,             // seconds to display
        FColor::Yellow,
        FString::Printf(TEXT("HP: %d / %d"), Hp, MaxHp));
}

Key -1 lets messages pile up; pass a stable non-negative integer (e.g. GetUniqueID()) to overwrite the same line each tick. Gate with #if !UE_BUILD_SHIPPING or remove before ship — this is PIE debugging only.

Where logs go

Destination How to reach it
Editor Output Log Window › Output Log; filter by category name
Log file on disk <Project>/Saved/Logs/<ProjectName>.log
Console (in-game) ~ key; Log CategoryName Verbosity to change level

Gotchas

  • %s with an FString without * — passes the FString object as a vararg, which reads garbage bytes; always write *MyString.
  • check in shipping is compiled to nothing — never put required side effects inside check(expr). If the call must execute, use verify(expr).
  • ensure fires only once per call site in a session by default — it will not spam, but a second hit in the same run is silently skipped. Use ensureAlways if every occurrence matters.
  • Logging in a tick at Log or Warning spams the log file and slows the editor; use Verbose/VeryVerbose for per-frame data, or UE_CLOG to conditionalize.
  • Compile-time verbosity too low — if your DECLARE_LOG_CATEGORY_EXTERN sets CompileTimeVerbosity to Warning, UE_LOG(Cat, Verbose, ...) is silently stripped at compile time regardless of runtime settings.
  • Fatal verbosity always runs, even when NO_LOGGING is defined — it maps to LowLevelFatalError which crashes unconditionally.
  • UE_LOGFMT requires the header — #include "Logging/StructuredLog.h"; missing it produces an undefined-macro error, not a useful diagnostic.

Version notes

  • UE_LOGFMT and UE_LOG_CONTEXT were introduced in UE 5.2 and are stable through 5.8.
  • The FStaticEnsureRecord per-call-site mechanism (atomic bGEnsureHasExecuted) replaced a simpler boolean in UE 5.x; the user-facing macro signatures are unchanged.
  • LowLevelFatalErrorHandler was deprecated in 5.7; use LowLevelFatalError (macro).

Cross-references

  • debugging-techniques — DrawDebug*, Visual Logger, Gameplay Debugger, native debugger usage; these complement logging for spatial and timeline debugging.

References & source material

Engine source (UE 5.8, under Engine/Source/Runtime/Core/Public/):

  • Logging/LogMacros.h — UE_LOG:278, UE_CLOG:325, FMsg::Logf:62, FMsg::Logf_Internal:72.
  • Logging/LogCategory.h — DECLARE_LOG_CATEGORY_EXTERN:133, DEFINE_LOG_CATEGORY:140, DEFINE_LOG_CATEGORY_STATIC:150 (moved here from LogMacros.h in 5.8).
  • Logging/LogVerbosity.h — ELogVerbosity::Type enum:16 (Fatal:22 through VeryVerbose:52, All:56, VerbosityMask:58).
  • Logging/LogCategory.h — FLogCategoryBase:21, FLogCategory<> template:86, IsSuppressed:45, SetVerbosity:59.
  • Logging/StructuredLog.h — UE_LOGFMT:46, UE_LOGFMT_EX:64, UE_LOG_CONTEXT:143, FLogRecord:181, SerializeForLog:297.
  • Misc/AssertionMacros.h — check:229, verify:226, checkf:259, verifyf:256, checkSlow:333, checkNoEntry:283, ensure:461, ensureMsgf:462, ensureAlways:463, ensureAlwaysMsgf:464, FDebug:68, LowLevelFatalError:591.

Official docs (UE 5.8, fetched and confirmed):

Deep-dive references in this skill: