Error handling

The Manager Service API error handling allows a specific service to report its own service-specific errors. Additionally, the internal MSA logic uses this error handling to communicate error situations.

A service can return an error to the client. The error status consists of an error number and an error text. The number is an element of a static list/enum and the text is variable. You can also send subsequent metadata together with the error status.

How sending works depends on the programming language.

  • C#, TypeScript and CTRL support the classic throw and try-catch syntax
  • In C++ a status object is returned
  • Optionally, the CTRL client can also return a status object

Error Handling Examples

On the CTRL Server:
Note:
We recommend implementing input argument checks to ensure type safety in all programming languages.

In the service implementation method, you can use the vrpcThrow function to return an MSA-specific error. While it is also possible to use throw, this approach provides less support on the client side.

   public anytype someServiceMethodImplWithArgCheck(VrpcServerContext &serverContext, anytype request)
  {
    // Verify argument type
    if (getType(request) != STRING_VAR)
      vrpcThrow(VrpcStatusCode::InvalidArgument, "Request must be a string");

    // Parse request
    string name = request;

    // Verify arguments
    if (name.isEmpty())
    {
      serverContext.addTrailingMetadata("error-details", "Some details");  // Optional set additional info
      vrpcThrow(VrpcStatusCode::InvalidArgument, "Name cannot be empty");
    }

    //...

    return "MyResult";
  }

In the service implementation method, you can return a Status object that contains an error.

   ::vrpc::Status someServiceMethodImplWithArgCheck(std::shared_ptr<::vrpc::ServerContext> serverContext, const Variable* request, Variable*& response)
  {
    // Verify argument type
    if (request == nullptr || request->isA(TEXT_VAR) != TEXT_VAR)
      return ::vrpc::Status(::vrpc::StatusCode::InvalidArgument, "Request must be a string");

    // Parse request
    TextVar name; name = *request;

    // Verify arguments
    if (name.getString().isEmpty())
    {
      serverContext->addTrailingHeader("error-details", "Some details");  // Optional set additional info
      return ::vrpc::Status(::vrpc::StatusCode::InvalidArgument, "Name cannot be empty");
    }

    //...

    response = new TextVar("MyResult");
    return vrpc::Status::OK;
  }              

In the service implementation method, you can throw any exception. However, we recommend using RpcException, as it provides the best support on the client side.

   public async Task<OaVariant> SomeServiceMethodImplWithArgCheck(OaVariant request, ServerCallContext serverContext)
  {
    // Verify argument type
    if (request.VariantType != OaVariantType.TextVar)
      throw new RpcException(new Status(StatusCode.InvalidArgument, "Request must be a string"));

    // Parse request
    string name = request.AsString();

    // Verify arguments
    if (name.Length == 0)
    {
      var trailers = new Metadata // Optional set additional info
      {
        { "error-details", "Some details" }
      };
      throw new RpcException(new Status(StatusCode.InvalidArgument, "Name cannot be empty"), trailers);
    }

    //...

    return new OaVariant("MyResult");
  }               

In the service implementation method, you can throw any error. However, we recommend using Vrpc.Error, as it provides the best possible support on the client side.

   private async someServiceMethodImplWithArgCheck(
    serverContext: Vrpc.ServerContext,
    request: Vrpc.Variant,
  ): Promise<Vrpc.Variant> {
    // Verify argument type
    if (!request.isString() || request.isNull())
      throw new Vrpc.Error(
        new Vrpc.Status(
          Vrpc.StatusCode.InvalidArgument,
          'Request must be a string',
        ),
      );

    // Parse request
    const name: string = request.getString();

    // Verify argument value
    if (name.length == 0) {
      const trailingMetadata = new Vrpc.Metadata();
      trailingMetadata.add('error-details', 'Some details'); // Optional set additional info
      throw new Vrpc.Error(
        new Vrpc.Status(
          Vrpc.StatusCode.InvalidArgument,
          'Name cannot be empty',
        ),
        trailingMetadata,
      );
    }

    //...

    return Vrpc.Variant.createString('MyResult');
  }            

On the Client:

As previously mentioned, the CTRL client supports two modes of error handling. By default, an exception is thrown when the client receives an error from the service. You can catch this error using a try-catch block along with the vrpcGetLastException function. Alternatively, you can change this behavior by calling setThrowExceptionOnError(false) on the stub object. This will return a status object in the response instead of throwing an exception.

  public void callSomeFunctionWithErrorHandling1(shared_ptr<VrpcStub> stub, anytype request)
  {
    try
    {
      stub.callFunction("someServiceMethodImplWithArgCheck", request);
    }
    catch
    {
      VrpcException ex = vrpcGetLastException();
      
      // Print error information
      Metadata trailers = ex.getTrailers();  // Error may send trailing headers
      string trailersStr = metadataToString(trailers);
      DebugN("MSA-specific Exception thrown: " + ex.getStatus().getStatusCode() + ", " + ex.getStatus().getText() + ", " + trailersStr);

    }

    //...
  }

  public void callSomeFunctionWithErrorHandling2(shared_ptr<VrpcStub> stub, anytype request)
  {
    stub.setThrowExceptionOnError(false);
    VrpcResponseData responseData = stub.callFunction("SomeFunction", request);

    VrpcStatus status = responseData.getStatus();

    if (!status.isOk())
    {
      // Print error information
      Metadata trailers = responseData.getTrailingHeaders();  // Error may send trailing headers
      string trailersStr = metadataToString(trailers);
      DebugN("Received Error Status: " + status.getStatusCode() + ", " + status.getText() + ", " + trailersStr);
    }

    //...
  }

  private string metadataToString(const Metadata &metadata)
  {
    string str;
    for (int i = 0; i < metadata.count(); i++)
    {
      MetadataEntry entry = metadata.getAt(i);
      str = str + "  " + entry.getKey() + " = " + (entry.isBinary() ? entry.getValueBinary() : entry.getValue());
    }
    return str;
  }
  void callSomeFunctionWithErrorHandling(std::unique_ptr<::vrpc::Stub> stub, const Variable& request)
  {
    std::shared_ptr<DefaultWaitForResponse> wait = std::make_shared<DefaultWaitForResponse>();
    stub->callFunctionWait("SomeFunction", request, wait);

    while (!wait->isFinished()) { Manager::dispatch(0.05); }
    const ::vrpc::ResponseData& responseData = wait->getResponseData();

    ::vrpc::Status status = responseData.getStatus();
    if (!status.isOk())
    {
      // Print error information
      const auto& trailingHeaders = responseData.getTrailingHeaders();  // Error may send trailing headers
      std::ostringstream trailersStream;
      for (const auto& item : trailingHeaders)
        trailersStream << "  " << item.getKey() << " = " << item.getValue();
      ErrHdl::error(ErrClass::PRIO_SEVERE, ErrClass::ERR_IMPL, ErrClass::UNEXPECTEDSTATE, "Received Error Status: ", status.toString(), trailersStream.str().c_str());
    }

    //...
  }                    
  public async Task CallSomeFunctionWithErrorHandling(VrpcStub stub, OaVariant request)
  {
    try
    {
      await stub.CallFunctionAsync("SomeFunction", request);
    }
    catch (RpcException ex)
    {
      // Print error information
      // Error may send trailing headers
      _logger.LogError($"MSA-specific Exception thrown: {ex.StatusCode}, {ex.Message}, {ex.TrailingHeaders}");
      throw new InvalidOperationException("SomeFunction failed", ex);
    }
    catch (Exception ex)
    {
      _logger.LogError($"Some Exception thrown: {ex.Message}");
      throw new InvalidOperationException("SomeFunction failed", ex);
    }

    //...
  }                
  public async callSomeFunctionWithErrorHandling(
    stub: Vrpc.Stub,
    request: Vrpc.Variant,
  ): Promise<void> {
    try {
      await stub.callFunction('SomeFunction', request);
    } catch (error) {
      // Print error information
      if (error instanceof Vrpc.Error) {
        var trailersString = '';
        if (error.trailingHeaders)
          trailersString = MetadataToString(
            'Received error Metadata',
            error.trailingHeaders,
          );
        console.error(
          `MSA-specific Error thrown: ${error.status.statusCode}, ${error.status.text}, ${trailersString}`,
        );
      } else if (error instanceof Error)
        console.error('Some Error thrown: ' + error.message);
      else console.error('Something else thrown: ' + error);
    }

    //...
  }