Custom Errors #

Pragma Engine supports two different error types: application errors and service errors.

  • Service errors indicate a critical error that requires attention.
  • Application errors handle expected errors.

Pragma Engine utilizes these errors throughout the engine, and custom ones can be used in plugins, player data modules, and custom services.

Service errors should always be used to indicate a critical error requiring further attention and not application errors.

Continue reading to learn more about what these errors are and how to author custom ones.

Service errors #

Service errors help indicate that an unexpected outcome has occurred. They alert the client SDK, game servers, and backend instances to help fix the error.

Custom service errors #

To create a new custom error, add a new enum type to the ExtError option in the 5-ext/ext-protos/src/main/proto/extensions/errorsExt.proto file.

enum ExtError {
   option (unreal_enum_name) = "ExtError";

   UNKNOWN_EXT_ERROR = 0;

   MY_CUSTOM_ERROR = 1;
}
Remember to run make ext-protos to register these changes before attempting to reference the new custom error.

To use the custom error, throw an ExtException with the new ExtError enum value as the error parameter.

throw ExtException(ExtError.MY_CUSTOM_ERROR, "something went wrong")

This is translated into a ServiceError returned to the client SDK. It can then be handled similarly to the standard provided PragmaErrors.

Application errors #

Application errors indicate that an expected or planned error type has occurred. These errors contain data that is sent back to the client and game servers to indicate why successful outcomes of a request couldn’t be completed.

For example, if a custom Party plugin has logic for incorporating players’ rank into matchmaking, a custom application error can return custom data back to the game client to show which pairs of players have incompatible ranks in the party.

Custom application errors #

Create and use custom application errors by following the steps below:

  1. Define the error’s protobufs.
  2. Utilize the errors with the application error exception class.
  3. Author logic in the PragmaSDK for checking the return types for an application error.

Continue reading the sections below for more implementation details.

Application error protobufs #

Application errors are defined using protobufs and can be authored anywhere under 5-ext/ext-protos/. Once defined, they are then used with the ApplicationErrorException class.

Every application error protobuf requires the Pragma defined option (force_include_type) = true field in order to ensure the error is properly generated. Run make ext-protos to generate the application error for use.

Below is an example proto message of an application error:

message DemoPluginApplicationError {
  option (force_include_type) = true;
  string message = 1;
}

Application error exception #

The ApplicationErrorException class is used to throw application errors. This class will contain a protbuf and is a RuntimeException().

class ApplicationErrorException(
    val errorMessage: GeneratedMessageV3
) : RuntimeException()

You can throw an ApplicationErrorException in any layer of Pragma Engine: plugins, custom services, etc. Below are functions available to help facilitate throwing application errors:

Helper functionDescription
applicationError(builder: () -> GeneratedMessageV3):Throws an application error. This function has one parameter–a block that returns an application error proto.
applicationRequire(value: Boolean, lazyError: ()Throws an application error if the condition value is false. This is similar to Kotlin’s require function but throws a ApplicationErrorException instead.
applicationRequireNotNull(value: T?, lazyError: () -> GeneratedMessageV3):Throws an application error if the value is null. This is similar to Kotlin’s requireNotNull function but throws a ApplicationErrorException instead.

The following example showcases using an application error in AccountPlugin.updateDisplayName().

override suspend fun updateDisplayName(requestedDisplayName: String, pragmaAccount: PragmaAccount) {
        applicationRequire(requestedDisplayName != "banneddisplayname") {
            DemoPluginApplicationError.newBuilder().setMessage("Inappropriate display name").build()
        }

    /// Logic for updating a display name …

}

Application error SDK #

Application errors are exposed as a PragmaResult failure. Once exposed, the types for an application error can be checked before reading in Unity and Unreal. The caller always needs to know the types of application errors that can be returned on the endpoint. Note that endpoints could have both Pragma defined and your own custom application errors.

Unreal

The Unreal PragmaSDK will return TPragmaResult as a failure from an application error. There are two functions available for acquiring specific data from the error:

MethodDescription
Result.GetErrorType(): StringRetrieves the class name of the error type.
Result.ReadError<EXPECTED-TYPE>()Retrieves the expected typed failure.

Below is an example of Account.UpdateDisplayName in the Unreal PragmaSDK getting a DemoPluginApplicationError back.

FPragma_Account_UpdateDisplayNameV1Request Request{ "banneddisplayname" };

Player->Session()->Account().Raw().UpdateDisplayNameV1(Request, (TPragmaResult<FPragma_Account_UpdateDisplayNameV1Response> Result, const FPragmaMessageMetadata&) {

	if (Result.IsTypedFailure() && Result.GetErrorType() ==
                FPragma_Account_DemoPluginApplicationError::StaticStruct())
	{
		auto DemoPluginApplicationError =
                   Result.ReadError<FPragma_Account_DemoPluginApplicationError>();
	}
}
Unity

The Unity PragmaSDK will return Result<TPayloadType> as a failure from an application error. You can check the type returned from the Result and read off the error.

Below is an example of calling Account.UpdateDisplayName through the Unity SDK with logic implemented for demoPluginApplicationError.

var request = new UpdateDisplayNameV1Request();
request.RequestedDisplayName = "banneddisplayname";

var future = player.Account.Raw.UpdateDisplayName(request);
yield return Utils.WithTimeout("UpdateDisplayName", future);

var result = future.Result;
if (result.IsFailure && result.ErrorType == typeof(DemoPluginApplicationError))
{
    var demoPluginApplicationError =
        result.ReadError<DemoPluginApplicationError>();
}

Service to service calls #

Application errors cannot be exposed through the Service.requestRpc API or PragmaNode.requestRpc API. If an application error is thrown from a call using these APIs, the call will return PragmaResult.Failure with ServiceError: PragmaError.Serialization_Error.

If you’re making a service to service call and need to be aware of any application errors that occur, use the APIs listed below.

API CallDescription
Service.requestRpcV2: PragmaResult<Response, PragmaFailure>Service request routed through a Service instance.

Common use cases: plugins and custom services.
Service.requestRpcV2AsPlayer: PragmaResult PragmaResult<Response, PragmaFailureService request routed through a Service instance. Only use this request if you have access to the player’s PlayerSession.

Common use case: custom services.
Service.requestRpcV2AsOperator: PragmaResult PragmaResult<Response, PragmaFailure>Service request routed through a Service instance. Only use this request if you have access to an operator’s OperatorSession.

Common use case: custom services.
PragmaNode.requestRpcV2: PragmaResult<TypeInfoAndSerializedMessage, ServiceError>Lower level service request routed through PragmaNode. The TypeInfoAndSerializedMessage will contain either the Response or an Application Error Proto

PragmaResult uses the PragmaFailure class to expose errors. PragmaFailure contains either a ServiceError or an application error proto in a TypeInfoAndSerializedMessage.

data class PragmaFailure(
    val serviceError: ServiceError? = null,
    val applicationError: TypeInfoAndSerializedMessage? = null
) {
    val isServiceError: Boolean = serviceError != null
    val isApplicationError: Boolean = applicationError != null
}

Below is an example of using the Service.requestRpcV2 API to handle success and failure cases:

// Fetching player data within a plugin or custom service 
val getPlayerDataRequest = GetServiceV1Request.newBuilder().setPlayerId(playerId).build()
val pragmaResult = service.requestRpcV2(getPlayerDataRequest, GetServiceV1Response::parseFrom)
pragmaResult.onFailure { pragmaFailure ->
    when {
        pragmaFailure.isServiceError -> {
            // A critical error occured - can log, rethrow, etc ...
        }
        pragmaFailure.isApplicationError -> {
            when (pragmaFailure.applicationError!!.parse()) {
                is DemoPlayerDataApplicationError -> { 
                    /* An expected application error occured - can use data off this proto, log an error, rethrow, etc ...*/ 
                }
                else -> { 
                    /* Some unknown application error occured - can log, rethrow, etc ... */
                }
            }
        }
    }
}
pragmaResult.onSuccess { response ->
    val entities = response.playerData.entitiesList
    // use entities as wanted ...
}