Skip to main content

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

  1. Get the interface for the area you need from ICFCore.
  2. Construct the delegate with CreateUObject, CreateWeakLambda or CreateLambda on the delegate type declared inside that interface.
  3. Call the function. It returns immediately.
  4. 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();
}));
danger

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.

SectionC++ error parameter
ICFCoreApi::Authentication(), ICFCoreApi::Authorized() and ICFCoreApi::Creation(), reached via Api()TOptional<FCFCoreApiResponseError>
Library() and ClientServerLibrary()TOptional<FCFCoreError>
ICFCore::Authentication(), the top-level accessorTOptional<FCFCoreError>
ICFCore::Creation(), the top-level accessorTOptional<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.

FieldTypeMeaning
isErrorboolfalse 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.
codeECFCoreErrorCodesThe SDK-level code. Defaults to None. This is what you switch on.
apiErrorFCFCoreApiResponseErrorPopulated only when code is ApiError. Meaningless otherwise.
descriptionFStringHuman-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.

info

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.

FieldTypeSet when
cancelledboolThe request was cancelled. description becomes Request Cancelled and nothing further is filled in.
badRequestboolHTTP 400.
entityNotFoundboolHTTP 404.
serverUnreachableboolA non-HTTP transport error, or HTTP 500, 502, 503 or 504.
missingPrivilegesboolHTTP 403. description becomes Missing user token or api key missing privileges to access end point.
tokenExpiredboolHTTP 401. description becomes User token has expired.
resourceExpiredboolHTTP 410 Gone. description becomes The resource expired.
failedToParseServerResponseboolThe response body could not be parsed into the expected model.
errorCodeint32The 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.
descriptionFStringAssembled 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 shapedescription
CancellationRequest Cancelled. Returns immediately, so no JSON is parsed and errorCode stays kNone.
Non-HTTP transport errorThe transport's own extra information followed by (serverUnreachable). Also returns immediately, so errorCode stays kNone.
HTTP status failureStarts 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.

SignalReading
serverUnreachable true and errorCode is kNoneTransport failure or a 5xx. The request was never judged. Preserve state, back off, retry.
errorCode is non-zeroThe backend parsed your request and rejected it. Permanent for that request. Fix the input, do not retry unchanged.
cancelled trueYour own cancellation. Not a fault.
failedToParseServerResponse trueThe 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.

warning

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.

warning

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.

ValueWhat it means
NoneNo error.
ApiErrorThe failure came from the CurseForge API. Read apiError.
FileSystemErrorA local filesystem operation failed.
FailedToInitializeThe SDK is not initialized, or initialization failed.
AlreadyInitializedInternal. Not produced.
NotImplementedInternal. Not produced.
InvalidModParamsMissing mod id, or the game id is invalid.
InstalledModNotFoundAn uninstalled mod was passed to a function that expects it installed, or it is corrupted.
InstallCancelledThe installation was cancelled.
DownloadCancelledThe download was cancelled.
UploadCancelledThe upload was cancelled.
MissingModsDirectoryFCFCoreSettings::modsDirectory was empty at initialization.
MissingModsDirectoryModeFCFCoreSettings::modsDirectoryMode was None at initialization.
MissingUserDataDirectoryFCFCoreSettings::userDataDirectory was empty at initialization.
MissingGameIdFCFCoreSettings::gameId was 0 at initialization.
MissingApiKeyFCFCoreSettings::apiKey was empty at initialization.
FailedToLoadModsStateFromDiskThe local installed-mods state could not be read.
FailedToSaveModsStateToDiskThe local installed-mods state could not be written.
MissingLatestFileInformationThe mod has no latest file information.
MissingFileInformationFile information is missing.
FileNotBelongingToModThe given FFile does not belong to the given FCFCoreMod.
NoPlatformFilesMatchedNo platform files could be matched from the server file list. Produced in ICFCoreClientServerLibrary flows.
MissingInstalledModsOne or more mods are missing. Produced in ICFCoreClientServerLibrary flows.
DetectedUnavailableModOne or more mods are set to unavailable on the CurseForge website.
ModAlreadyBeingInstalledAn installation for that mod is already running.
FailedToDownloadFileThe download failed.
DownloadedFileHasInvalidHashThe downloaded file did not match its expected hash.
FailedDeletingOutputDirectoryThe output directory could not be deleted.
FailedDeletingOutputFileThe output file could not be deleted.
FailedToUnzipExtraction failed.
FailedToMoveModDirectoryThe mod directory could not be moved into place.
FailedSettingAuthTokenThe auth token could not be stored.
UserNotAuthenticatedThe operation requires a signed-in player.
FailedToCancelActionThe cancellation request itself failed.
MissingParameterA required parameter was not supplied.
FailedToVerifySignatureA signed server response, such as a premium mods check, could not be verified.
ModsNotOwnedByUserOne or more mods are not owned and must be purchased before use.
MissingModsDetailsMod details could not be retrieved, for example for a mod that is not on the CurseForge servers.
BindiffInvalidHeaderA binary diff patch had an invalid header.
BindiffFailedToApplyPatchA binary diff patch could not be applied.
UnsupportedStrategyInternal. A mod installation strategy is not supported for that request. No game-side handling required.
UnsupportedSchemaVersionInternal. Usually means the server returned data this SDK version does not support. No game-side handling required.
UnsupportedDiffMethodThe diff method is not supported.
FailedToZipCompression failed.
AlreadySentUserLogsInternal. Not produced.
SubscriptionSyncInProgressAnother subscription sync is already running. Wait for it to complete.
PurchasePollingTimeoutPollModPurchase hit its maximum polling duration without detecting ownership.
PurchasePollingStoppedPollModPurchase was stopped through StopPurchasePolling before ownership was detected.
FailedToDecryptEncrypted 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.

ValueConstantWhat it means
0kNoneNo error.
1100kNoSuchModUploading a file for, or updating, a mod that does not exist.
1102kNoSuchGameCreating a mod with an unknown game id.
1103kNoSuchModFileAn uploaded mod file failed with an internal server error, or a manually cooked file was uploaded for an unknown source file id.
1104kModGameMismatchUploading a mod file to a mod belonging to a different game than the one the SDK was initialized with.
1105kModFileSourceMismatchMod file source mismatch.
1106kNotSourceModFileThe referenced file is not a source mod file.
1107kModFileAuthorMismatchOnly the author who uploaded the source file may upload cooked files associated with it.
1110kUserNotAllowedForModThe signed-in account may not upload or update this mod.
1111kDeletedUserSuspectedDeleted user suspected.
1120kModIsNotValidThe mod is not valid.
1121kModNameAlreadyExistsCreating a new mod with a name that is already taken.
1130kNoUploadToClassThe class (classId) is configured to not allow uploads.
1131kFileIsStillProcessingThe file is still processing.
1132kFileSizeTooBigExceeded the maximum file size.
1133kNoFileNameMissing filename for the uploaded file.
1134kNoAdditionalFilesNo additional files.
1135kParentFileDoesNotExistThe parent file does not exist.
1136kGameVersionsNotAllowedForAddonFilesGame versions are not allowed for addon files.
1137kNoSuchGameVersionUnknown game version. Re-fetch the version list and correct the version id.
1138kInvalidGameVersionInvalid game version. Re-fetch the version list and correct the version id.
1139kGameVersionNotMappedToCategoryThe game version is not allowed for the given class (classId).
1140kNoChildFileTypeNo child file type.
1141kUserCannotUploadMemberAccessOnlyContentThe mod author may not upload membership files (CurseForge Pro).
1142kUserCannotUploadFilesThe signed-in account lacks privileges to upload a file for this mod.
1143kSourceFileNotInCookingStatusThe source file is not in cooking status.
1144kNotAllSourceFilesReadyNot all source files are ready.
1145kSourceFileAlreadyHasPlatformThe source file already has a file for that platform.
1160kPlatformNotAllowedThe 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.

NodeBehaviour when the SDK is not initialized
InitializeRuns. This is the call that initializes.
UninitializeRuns and reports success. Uninitialize on an uninitialized SDK is a no-op, so it is safe to call unconditionally.
Every node with an error pinThe 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 ManagementChecks initialization and returns silently. The node has no output pins.
Is AuthenticatedChecks 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 PollingNo initialization check. It only guards against a null context.
Zip PathsNo initialization check. Compression does not depend on SDK initialization.
note

Update Settings is one of the guarded nodes in the first row above, and behaves the same way when called before initialization completes.

warning

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.

note

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.

danger

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.

ExceptionWhat 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

  1. Turn the logger on in settings. It is disabled by default.
  2. 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.
  3. The thread writes to a rolling file in a logs subfolder of userDataDirectory.
  4. 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 FCFCoreSettingsLoggerDefaultAccepted rangeMeaning
enabledfalsetrue or falseWhen enabled, logs are written to local disk under userDataDirectory
history8Clamped 0 to 30How many rolled files to keep on disk
maxSizeInMB2Clamped 2 to 10Maximum size of one log file before rolling, in megabytes
warning

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;
warning

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

SituationWhere to go
Initialization keeps failing with MissingModsDirectory, MissingModsDirectoryMode or MissingUserDataDirectoryYour own settings. Nothing has reached CurseForge yet.
missingPrivileges (403) on endpoints you expect to work, or a feature that needs enabling on your game recordCheck 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 actionEmail 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 explainEmail cfforstudios@overwolf.com with the log files from <userDataDirectory>/logs/ attached