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_LOGFMTcalls with the right verbosity for the context. - Choosing between
check,verify, andensurefor 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
%swith anFStringwithout*— passes theFStringobject as a vararg, which reads garbage bytes; always write*MyString.checkin shipping is compiled to nothing — never put required side effects insidecheck(expr). If the call must execute, useverify(expr).ensurefires 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. UseensureAlwaysif every occurrence matters.- Logging in a tick at
LogorWarningspams the log file and slows the editor; useVerbose/VeryVerbosefor per-frame data, orUE_CLOGto conditionalize. - Compile-time verbosity too low — if your
DECLARE_LOG_CATEGORY_EXTERNsetsCompileTimeVerbositytoWarning,UE_LOG(Cat, Verbose, ...)is silently stripped at compile time regardless of runtime settings. Fatalverbosity always runs, even whenNO_LOGGINGis defined — it maps toLowLevelFatalErrorwhich crashes unconditionally.UE_LOGFMTrequires the header —#include "Logging/StructuredLog.h"; missing it produces an undefined-macro error, not a useful diagnostic.
Version notes
UE_LOGFMTandUE_LOG_CONTEXTwere introduced in UE 5.2 and are stable through 5.8.- The
FStaticEnsureRecordper-call-site mechanism (atomicbGEnsureHasExecuted) replaced a simpler boolean in UE 5.x; the user-facing macro signatures are unchanged. LowLevelFatalErrorHandlerwas deprecated in 5.7; useLowLevelFatalError(macro).
Cross-references
ue-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 fromLogMacros.hin 5.8).Logging/LogVerbosity.h—ELogVerbosity::Typeenum:16 (Fatal:22 throughVeryVerbose: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):
- Logging in Unreal Engine — https://dev.epicgames.com/documentation/unreal-engine/logging-in-unreal-engine
- Asserts in Unreal Engine — https://dev.epicgames.com/documentation/unreal-engine/asserts-in-unreal-engine
Deep-dive references in this skill:
- references/log-categories-and-verbosity.md — full category declaration patterns, verbosity table, runtime/build-time filtering, console and command-line controls.
- references/assertions.md — compile flags (
DO_CHECK,DO_ENSURE),USE_CHECKS_IN_SHIPPING,FDebughelpers, shipping behavior details. - references/structured-logging.md —
UE_LOGFMTpositional vs named fields,UE_LOG_CONTEXTthread-local scopes,FLogRecord,SerializeForLogcustomization.