Error handling and logging
Every operation in the CurseForge SDK for Unreal is asynchronous, and every failure arrives through one of two error structs. This page covers the shape of an async call, both error types field by field, the complete code tables you switch on, and the file logger to enable in shipped builds so a failed install leaves evidence.
Everything here describes cfcore-sdk-ue. For the plugin layout, the C++ and Blueprint entry points and the initialization sequence, see the Unreal overview.
The shape of an async operation
You pass in one or more delegates, the call returns void immediately, and exactly the delegates that apply are executed later on the game thread. There is no future or awaitable in the public surface except in the compression service, and there is no event loop to pump: the plugin drives its own threads and marshals completions back for you.
In C++ the convention is TOptional on both sides: TOptional<Payload> and TOptional<Error>. The error being unset is what makes the payload safe to read.
How it works
- Get the interface for the area you need from
ICFCore. - Construct the delegate with
CreateUObject,CreateWeakLambdaorCreateLambdaon the delegate type declared inside that interface. - Call the function. It returns immediately.
- In the callback, test the error first. Only if the error is unset, read the payload with
GetValue().
#include <cfcore_context.h>
using namespace cfcore;
// Library-style call: payload plus FCFCoreError.
CFCoreContext::GetInstance()->Library()->GetModsDirInfo(
ICFCoreLibrary::FGetModsDirInfoDelegate::CreateLambda(
[](const TOptional<FModsDirInfo>& opt_info,
const TOptional<FCFCoreError>& opt_err) {
if (opt_err.IsSet() || !opt_info.IsSet()) {
return; // Branch on opt_err.GetValue().code
}
const FModsDirInfo& info = opt_info.GetValue();
}));
// Api-style call: payload plus FCFCoreApiResponseError.
CFCoreContext::GetInstance()->Api()->GetMod(
mod_id,
ICFCoreApi::FGetModDelegate::CreateLambda(
[](TOptional<FCFCoreMod> opt_mod,
const TOptional<FCFCoreApiResponseError>& opt_err) {
if (opt_err.IsSet()) {
const FCFCoreApiResponseError& api_err = opt_err.GetValue();
if (api_err.tokenExpired) {
// The local token is already erased. Re-authenticate.
} else if (api_err.serverUnreachable) {
// Transient. Retry later, do not show a hard failure.
} else if (api_err.errorCode == api_error_codes::kNoSuchMod) {
// Permanent. Remove it from your UI.
}
return;
}
const FCFCoreMod& mod = opt_mod.GetValue();
}));
The SDK does not cancel in-flight API requests on uninitialize, so a callback can still fire after teardown. Capturing a raw this in a lambda is a use-after-free waiting to happen. Prefer CreateUObject or CreateWeakLambda.
Blueprint: the same operations are single nodes with the branches already split. Get Mod Info By Id (category cfcore|Api) has pins modId, on_mod and on_error. Get Mods Directory Info (category cfcore|Library) has pins OnModsDirInfo and OnError. Bind each delegate pin to its own Custom Event node. Every delegate is invoked with ExecuteIfBound, so an unconnected pin discards the result in silence, which is the most common cause of "the node does nothing".
Which error type you get
Which struct you get depends on the C++ interface, not on the kind of failure. FCFCoreApiResponseError carries HTTP-shaped information that FCFCoreError only carries as a nested struct.
| Section | C++ error parameter |
|---|---|
ICFCoreApi::Authentication(), ICFCoreApi::Authorized() and ICFCoreApi::Creation(), reached via Api() | TOptional<FCFCoreApiResponseError> |
Library() and ClientServerLibrary() | TOptional<FCFCoreError> |
ICFCore::Authentication(), the top-level accessor | TOptional<FCFCoreError> |
ICFCore::Creation(), the top-level accessor | TOptional<FCFCoreError> |
PremiumMods() | TOptional<FCFCoreError> |
Subscription() | TOptional<FCFCoreError> |
Analytics() | None. The three send methods return bool synchronously |
Utils()->Compression() | None. Zip and Unzip return TFuture<ECompressionError> |
The Blueprint layer flattens this. Every Blueprint error pin carries FCFCoreError. When the underlying failure was an API failure, the subsystem sets code to ApiError, copies the whole API error into apiError and copies its description up into description. The three analytics nodes (Send Game Play Session Analytic, Send Mod Browsing Funnel Impression Analytic, Send Mod Browsing Funnel Action Analytic) bridge their bool return by firing OnError with a bare ApiError code and no further detail, so read the log when one of those fails.
FCFCoreError
The universal error struct. All four fields are BlueprintReadOnly, so all four are readable from a Blueprint break node.
| Field | Type | Meaning |
|---|---|---|
isError | bool | false on a default-constructed error. Set to true by every constructor that receives an actual failure. Check this when you were handed an FCFCoreError by value rather than a TOptional. |
code | ECFCoreErrorCodes | The SDK-level code. Defaults to None. This is what you switch on. |
apiError | FCFCoreApiResponseError | Populated only when code is ApiError. Meaningless otherwise. |
description | FString | Human-readable text. When the failure came from the API this is the API error description copied up. Log it, do not parse it. |
An FCFCoreError can legitimately arrive with isError == false.
When code is ApiError, the code alone tells you nothing actionable. Read apiError next.
FCFCoreApiResponseError
The API-level error. Eight of its ten fields are booleans, more than one can be relevant at a time. Check them in order of specificity.
| Field | Type | Set when |
|---|---|---|
cancelled | bool | The request was cancelled. description becomes Request Cancelled and nothing further is filled in. |
badRequest | bool | HTTP 400. |
entityNotFound | bool | HTTP 404. |
serverUnreachable | bool | A non-HTTP transport error, or HTTP 500, 502, 503 or 504. |
missingPrivileges | bool | HTTP 403. description becomes Missing user token or api key missing privileges to access end point. |
tokenExpired | bool | HTTP 401. description becomes User token has expired. |
resourceExpired | bool | HTTP 410 Gone. description becomes The resource expired. |
failedToParseServerResponse | bool | The response body could not be parsed into the expected model. |
errorCode | int32 | The CurseForge API error code from the JSON body, defaulting to kNone (0). Filled in only for an HTTP-status failure whose response content type starts with application/json and whose body parses. |
description | FString | Assembled per the rules below. |
Any status below 200 or at 400 and above is treated as an error. A status that is not 400, 401, 403, 404, 410, 500, 502, 503 or 504 sets no boolean at all: you get the generic description and, if the body was JSON, an errorCode.
| Failure shape | description |
|---|---|
| Cancellation | Request Cancelled. Returns immediately, so no JSON is parsed and errorCode stays kNone. |
| Non-HTTP transport error | The transport's own extra information followed by (serverUnreachable). Also returns immediately, so errorCode stays kNone. |
| HTTP status failure | Starts as Failed to send request to server with error: {status code}, is replaced by the specific message for 401, 403 and 410, then replaced again by the API's own message field when the JSON body carries a non-empty one. |
Transport failure versus backend rejection
The two need opposite recovery, and the fields tell them apart.
| Signal | Reading |
|---|---|
serverUnreachable true and errorCode is kNone | Transport failure or a 5xx. The request was never judged. Preserve state, back off, retry. |
errorCode is non-zero | The backend parsed your request and rejected it. Permanent for that request. Fix the input, do not retry unchanged. |
cancelled true | Your own cancellation. Not a fault. |
failedToParseServerResponse true | The call reached the backend and came back malformed for this SDK version. Retrying does not help. Log it and escalate. |
Worked example. A mod creation upload failing with badRequest and errorCode kModNameAlreadyExists (1121) needs the create-mod form kept open with the name field flagged. The same call failing with serverUnreachable and errorCode kNone needs the opposite: nothing about the request was wrong, so hold the form state and retry later.
A 401 has a side effect. When tokenExpired is set, the SDK erases the locally stored auth token before your callback runs. There is nothing to clean up, but the player is now unauthenticated, so re-run your authentication flow instead of retrying the call.
errorCode is an int32 and the codes are const int32 constants in the C++ namespace cfcore::api_error_codes, not a UENUM. Blueprint sees a bare integer with no display names, so a Blueprint switch on an API error code is a switch on a magic number. Do that branching in C++ against the named constants.
ECFCoreErrorCodes reference
The complete enumeration. Switch on the enumerator, never on the integer: only None = 0 is pinned to an explicit value, so every other value is positional and can shift in a future SDK version.
| Value | What it means |
|---|---|
None | No error. |
ApiError | The failure came from the CurseForge API. Read apiError. |
FileSystemError | A local filesystem operation failed. |
FailedToInitialize | The SDK is not initialized, or initialization failed. |
AlreadyInitialized | Internal. Not produced. |
NotImplemented | Internal. Not produced. |
InvalidModParams | Missing mod id, or the game id is invalid. |
InstalledModNotFound | An uninstalled mod was passed to a function that expects it installed, or it is corrupted. |
InstallCancelled | The installation was cancelled. |
DownloadCancelled | The download was cancelled. |
UploadCancelled | The upload was cancelled. |
MissingModsDirectory | FCFCoreSettings::modsDirectory was empty at initialization. |
MissingModsDirectoryMode | FCFCoreSettings::modsDirectoryMode was None at initialization. |
MissingUserDataDirectory | FCFCoreSettings::userDataDirectory was empty at initialization. |
MissingGameId | FCFCoreSettings::gameId was 0 at initialization. |
MissingApiKey | FCFCoreSettings::apiKey was empty at initialization. |
FailedToLoadModsStateFromDisk | The local installed-mods state could not be read. |
FailedToSaveModsStateToDisk | The local installed-mods state could not be written. |
MissingLatestFileInformation | The mod has no latest file information. |
MissingFileInformation | File information is missing. |
FileNotBelongingToMod | The given FFile does not belong to the given FCFCoreMod. |
NoPlatformFilesMatched | No platform files could be matched from the server file list. Produced in ICFCoreClientServerLibrary flows. |
MissingInstalledMods | One or more mods are missing. Produced in ICFCoreClientServerLibrary flows. |
DetectedUnavailableMod | One or more mods are set to unavailable on the CurseForge website. |
ModAlreadyBeingInstalled | An installation for that mod is already running. |
FailedToDownloadFile | The download failed. |
DownloadedFileHasInvalidHash | The downloaded file did not match its expected hash. |
FailedDeletingOutputDirectory | The output directory could not be deleted. |
FailedDeletingOutputFile | The output file could not be deleted. |
FailedToUnzip | Extraction failed. |
FailedToMoveModDirectory | The mod directory could not be moved into place. |
FailedSettingAuthToken | The auth token could not be stored. |
UserNotAuthenticated | The operation requires a signed-in player. |
FailedToCancelAction | The cancellation request itself failed. |
MissingParameter | A required parameter was not supplied. |
FailedToVerifySignature | A signed server response, such as a premium mods check, could not be verified. |
ModsNotOwnedByUser | One or more mods are not owned and must be purchased before use. |
MissingModsDetails | Mod details could not be retrieved, for example for a mod that is not on the CurseForge servers. |
BindiffInvalidHeader | A binary diff patch had an invalid header. |
BindiffFailedToApplyPatch | A binary diff patch could not be applied. |
UnsupportedStrategy | Internal. A mod installation strategy is not supported for that request. No game-side handling required. |
UnsupportedSchemaVersion | Internal. Usually means the server returned data this SDK version does not support. No game-side handling required. |
UnsupportedDiffMethod | The diff method is not supported. |
FailedToZip | Compression failed. |
AlreadySentUserLogs | Internal. Not produced. |
SubscriptionSyncInProgress | Another subscription sync is already running. Wait for it to complete. |
PurchasePollingTimeout | PollModPurchase hit its maximum polling duration without detecting ownership. |
PurchasePollingStopped | PollModPurchase was stopped through StopPurchasePolling before ownership was detected. |
FailedToDecrypt | Encrypted server data could not be decrypted, such as the guid and key pairs from GetPremiumModFileDetails. |
API error codes reference
These are the const int32 constants in cfcore::api_error_codes, surfaced through FCFCoreApiResponseError::errorCode. Unlike ECFCoreErrorCodes these values are explicit and stable, so they are safe to compare against.
| Value | Constant | What it means |
|---|---|---|
| 0 | kNone | No error. |
| 1100 | kNoSuchMod | Uploading a file for, or updating, a mod that does not exist. |
| 1102 | kNoSuchGame | Creating a mod with an unknown game id. |
| 1103 | kNoSuchModFile | An uploaded mod file failed with an internal server error, or a manually cooked file was uploaded for an unknown source file id. |
| 1104 | kModGameMismatch | Uploading a mod file to a mod belonging to a different game than the one the SDK was initialized with. |
| 1105 | kModFileSourceMismatch | Mod file source mismatch. |
| 1106 | kNotSourceModFile | The referenced file is not a source mod file. |
| 1107 | kModFileAuthorMismatch | Only the author who uploaded the source file may upload cooked files associated with it. |
| 1110 | kUserNotAllowedForMod | The signed-in account may not upload or update this mod. |
| 1111 | kDeletedUserSuspected | Deleted user suspected. |
| 1120 | kModIsNotValid | The mod is not valid. |
| 1121 | kModNameAlreadyExists | Creating a new mod with a name that is already taken. |
| 1130 | kNoUploadToClass | The class (classId) is configured to not allow uploads. |
| 1131 | kFileIsStillProcessing | The file is still processing. |
| 1132 | kFileSizeTooBig | Exceeded the maximum file size. |
| 1133 | kNoFileName | Missing filename for the uploaded file. |
| 1134 | kNoAdditionalFiles | No additional files. |
| 1135 | kParentFileDoesNotExist | The parent file does not exist. |
| 1136 | kGameVersionsNotAllowedForAddonFiles | Game versions are not allowed for addon files. |
| 1137 | kNoSuchGameVersion | Unknown game version. Re-fetch the version list and correct the version id. |
| 1138 | kInvalidGameVersion | Invalid game version. Re-fetch the version list and correct the version id. |
| 1139 | kGameVersionNotMappedToCategory | The game version is not allowed for the given class (classId). |
| 1140 | kNoChildFileType | No child file type. |
| 1141 | kUserCannotUploadMemberAccessOnlyContent | The mod author may not upload membership files (CurseForge Pro). |
| 1142 | kUserCannotUploadFiles | The signed-in account lacks privileges to upload a file for this mod. |
| 1143 | kSourceFileNotInCookingStatus | The source file is not in cooking status. |
| 1144 | kNotAllSourceFilesReady | Not all source files are ready. |
| 1145 | kSourceFileAlreadyHasPlatform | The source file already has a file for that platform. |
| 1160 | kPlatformNotAllowed | The requested platform is not allowed. |
Blueprint: there is no Blueprint-side equivalent of these constants. Break the error pin, break its apiError member, and errorCode appears as a plain integer. Compare against the numbers in the first column, or move the branching into C++.
The upload and cooking codes above (1103, 1107, 1143, 1144, 1145) belong to the cloud cooking pipeline. Its own status and error enums are documented in cloud cooking references.
The initialization guard
Almost every Blueprint node on UCFCoreSubsystem checks ICFCore::IsInitialized() before it does anything. An uninitialized SDK does not do nothing: it fires the error pin.
| Node | Behaviour when the SDK is not initialized |
|---|---|
| Initialize | Runs. This is the call that initializes. |
| Uninitialize | Runs and reports success. Uninitialize on an uninitialized SDK is a no-op, so it is safe to call unconditionally. |
| Every node with an error pin | The call never reaches the SDK. The error pin fires immediately with code set to FailedToInitialize, isError set to true and description set to the literal string Not initialized. |
| Disable Auto Management | Checks initialization and returns silently. The node has no output pins. |
| Is Authenticated | Checks initialization and fires on_is_auth with false. That is indistinguishable from a signed-out player, so track initialization yourself if you need to tell them apart. |
| Stop Purchase Polling | No initialization check. It only guards against a null context. |
| Zip Paths | No initialization check. Compression does not depend on SDK initialization. |
Update Settings is one of the guarded nodes in the first row above, and behaves the same way when called before initialization completes.
If the error pin is unconnected on a guarded node, nothing happens at all and the call vanishes. Wire the error pin on every node.
There is no Blueprint equivalent of ICFCore::IsInitialized(). From Blueprint, either track initialization yourself or treat a FailedToInitialize error as the signal. In C++, IsInitialized() and ICFCoreAuthentication::IsAuthenticated() are the only synchronous accessors in the surface.
In C++ the guard does not exist, and the failure is a crash
The table above describes the Blueprint layer. C++ has no equivalent protection, and this is the most dangerous difference between the two bindings.
Every ICFCore sub-interface accessor returns nullptr while the SDK is uninitialized: Api(), Library(), Authentication(), Creation(), PremiumMods(), Analytics() and Subscription() each test an internal initialized flag and return nullptr when it is false. Calling a method through one of those null pointers is undefined behaviour and in practice an access violation. No error code is produced, because no SDK code runs.
Utils() is the only accessor with no initialization check, so compression is available at any time.
Null-check the accessor itself, every time, rather than relying on an earlier IsInitialized() result. Initialization is asynchronous, so it can complete or fail between your check and your call.
ICFCoreApi* api = CFCoreContext::GetInstance()->Api();
if (!api) {
// Uninitialized. There is no error to report and nothing to call.
return;
}
This is why a C++ integration should route startup through one place that owns initialization state, rather than fetching accessors ad hoc across the codebase.
Convention exceptions
Four places break the SDK's own pattern.
| Exception | What to do instead |
|---|---|
ICFCoreLibrary::FGetInstalledModsDelegate takes only const TArray<FInstalledMod>&, with no error parameter. | Treat an empty array as "nothing installed", not "call failed". The Get Installed Mods node still has an error pin because the subsystem adds the initialization guard. Same for Get Global Installed Mods and ICFCoreClientServerLibrary::FGetInstalledModsDelegate. |
Progress delegates (ICFCoreLibrary::FInstallProgressDelegate, ICFCoreCreation::FFileTransferProgressDelegate, ICompressionService::FProgressDelegate) carry no error. | Failures arrive on the separate completion delegate, never on progress. |
ICFCorePremiumMods::PollModPurchase has extra terminal outcomes. Poll Mod Purchase takes OnPurchased, OnError, OnPurchasePollingTimeout and OnPurchasePollingStopped, and the last two fire instead of OnError. | Handle timeout and cancellation as their own outcomes. In C++ the single FPollPurchaseDelegate reports them as the PurchasePollingTimeout and PurchasePollingStopped codes. Starting a new polling session while one is running stops the previous one without firing its delegate at all. |
ICompressionService returns TFuture<ECompressionError> instead of taking a completion delegate. | The Zip Paths node restores the usual shape with OnProgress, OnSuccess and an OnError pin carrying an ECompressionError, not an FCFCoreError. |
Status enums
Game info and platforms reconciles ECFCoreStatus and ECFCoreApiStatus, the game record's lifecycle and visibility statuses. Installing and managing mods documents EInstalledModStatus, the load gate that determines whether an installed mod is safe to load.
The rest of the status vocabulary, so you know where to look: ELibraryProgressState (the stage one install or upload is in, library/models/enums/library_progress_state.h), EModsUpdateProgressState (a client or server bulk update, mods_update_progress_state.h), ECFCoreModStatus (api/models/enums/mod_status.h) and ECFCoreFileStatus (file_status.h) for server-side moderation status, and ECompressionError (None, FailedToReadZip, FailedToExtractFile, FailedToWriteFile). Moderation states and what they mean for visibility are covered in moderation.
Logging to disk
UE_LOG output is frequently not written to disk in shipping builds, so by default you have no evidence when a player cannot install a mod. The SDK ships its own file logger for that reason, and it is off by default.
All SDK lines go to the Unreal log under the LogCFCore category, declared in common/cfcore_log.h. Enabling the file logger adds a second destination without removing the first. Filter on LogCFCore to read the SDK's output, and log your game's own messages to your own category.
How it works
- Turn the logger on in settings. It is disabled by default.
- At initialization the SDK starts a dedicated logger thread. Calls from any thread are enqueued rather than written inline, so logging never blocks the game thread.
- The thread writes to a rolling file in a
logssubfolder ofuserDataDirectory. - When the current file passes the size limit, the logger rolls to a new file and prunes the oldest files beyond the history count.
Field on FCFCoreSettingsLogger | Default | Accepted range | Meaning |
|---|---|---|---|
enabled | false | true or false | When enabled, logs are written to local disk under userDataDirectory |
history | 8 | Clamped 0 to 30 | How many rolled files to keep on disk |
maxSizeInMB | 2 | Clamped 2 to 10 | Maximum size of one log file before rolling, in megabytes |
history and maxSizeInMB are clamped, not rejected. Ask for 100 files of 500 MB and you silently get 30 files of 10 MB, with no error and no warning.
Files are named game_<gameId>_<UTC timestamp>.log and land in <userDataDirectory>/logs/. Each line carries a UTC timestamp, the frame counter, the verbosity, the message, and the source file and line. Because userDataDirectory accepts the escape tokens (%USER_DIR%, %USER_SETTINGS_DIR%, %PROJECT_DIR%, %PROJECT_SAVED_DIR%), the resolved path is predictable enough to ask a player for.
settings.userDataDirectory = TEXT("%USER_SETTINGS_DIR%/my_game/cfcore/user_data");
// Logs land in %USER_SETTINGS_DIR%/my_game/cfcore/user_data/logs/
settings.logger.enabled = true;
settings.logger.history = 8;
settings.logger.maxSizeInMB = 10;
Enable this in shipping builds. Without it, an installation failure reported by a player is unreproducible.
Blueprint: no node toggles the logger. Set enabled, history and maxSizeInMB under Project Settings > Plugins > CFCore, then initialize from the project-config settings so those values are picked up.
When to escalate
| Situation | Where to go |
|---|---|
Initialization keeps failing with MissingModsDirectory, MissingModsDirectoryMode or MissingUserDataDirectory | Your own settings. Nothing has reached CurseForge yet. |
missingPrivileges (403) on endpoints you expect to work, or a feature that needs enabling on your game record | Check the game configuration in the Developer Portal at console.curseforge.com |
kFileSizeTooBig (1132) and you need the limit raised, kPlatformNotAllowed (1160), or a code in the 1100 range you cannot map to an action | Email cfforstudios@overwolf.com with the mod id, file id, and the exact errorCode and description |
A reproducible crash, or a FailedToVerifySignature or FailedToDecrypt you cannot explain | Email cfforstudios@overwolf.com with the log files from <userDataDirectory>/logs/ attached |