dotnet/aspnetcore · error · IllegalArgumentException

Expected either 'error' or 'result' to be provided, but not…

Error message

Expected either 'error' or 'result' to be provided, but not both.

What it means

Thrown by the CompletionMessage constructor when both error and result are non-null. A CompletionMessage is the wire reply to an invocation; the protocol allows either a successful result OR an error string, never both. This is an internal invariant guard - applications rarely construct CompletionMessage directly unless writing a custom hub protocol or a test server.

Solutions

  1. When building a CompletionMessage, choose exactly one: pass result with error==null, or error with result==null.
  2. Add a helper that encodes the rule: CompletionMessage.success(invId, result) vs CompletionMessage.error(invId, err).
  3. If the message came from the wire, validate at the parser: route to error vs result branch before constructing.

Example fix

// before
new CompletionMessage(headers, invId, result, "oops"); // both set -> throws

// after
CompletionMessage msg = error != null
  ? new CompletionMessage(headers, invId, null, error)
  : new CompletionMessage(headers, invId, result, null);
Defensive patterns

Strategy: validation

Validate before calling

CompletionMessage success(String invId, Object result) {
  return new CompletionMessage(null, invId, result, null);
}
CompletionMessage failure(String invId, String error) {
  return new CompletionMessage(null, invId, null, error);
}

// always pick one branch via the helper - never pass both.

Type guard

boolean isValidCompletionInputs(Object result, String error) {
  return result == null || error == null; // at most one non-null
}

Try / catch

try {
  new CompletionMessage(headers, invId, result, error);
} catch (IllegalArgumentException e) {
  if (e.getMessage().contains("either 'error' or 'result'")) {
    // choose success or error branch and rebuild
  } else throw e;
}

Prevention

When it happens

Trigger: Constructing new CompletionMessage(headers, invocationId, resultObj, errorString) with both the result and error arguments non-null. Happens in custom HubProtocol implementations, test fixtures that craft replies, or buggy server-side code that mirrors CompletionMessage.

Common situations: Test doubles for the server that fill both fields by mistake; a custom protocol adapter that copies fields without choosing one branch; upgrading the protocol and forgetting the mutual-exclusion rule.

Related errors


AI-assisted analysis of dotnet/aspnetcore@3600ca084e (2026-08-11). Data as JSON: /api/errors/48636df05c0d5089. Report an issue: GitHub.

Appendix: source

Thrown at src/SignalR/clients/java/signalr/core/src/main/java/com/microsoft/signalr/CompletionMessage.java:20

// The .NET Foundation licenses this file to you under the MIT license.

package com.microsoft.signalr;

import java.util.Map;

public final class CompletionMessage extends HubMessage {
    private final int type = HubMessageType.COMPLETION.value;
    private Map<String, String> headers;
    private final String invocationId;
    private final Object result;
    private final String error;

    public CompletionMessage(Map<String, String> headers, String invocationId, Object result, String error) {
        if (headers != null && !headers.isEmpty()) {
            this.headers = headers;
        }
        if (error != null && result != null) {
            throw new IllegalArgumentException("Expected either 'error' or 'result' to be provided, but not both.");
        }
        this.invocationId = invocationId;
        this.result = result;
        this.error = error;
    }

    public Map<String, String> getHeaders() {
        return headers;
    }

    public Object getResult() {
        return result;
    }

    public String getError() {
        return error;
    }

View on GitHub (pinned to 3600ca084e)