Errors
Similar to the familiar "404 Not Found" and "500 Internal Server Error" status codes you may have seen in HTTP, Connect uses a set of 16 error codes. In the Connect protocol, an error is always represented as JSON, and is easily readable in the developer tools of your browser. For example:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"code": "invalid_argument",
"message": "sentence cannot be empty"
}
With the gRPC-web protocol, errors are usually not human-readable, but Connect provides a common type that represents errors consistently across all supported protocols.
All errors are represented by ConnectError
,
a subtype of the built-in Error
class.
Error codes
The code
property holds one of Connect's error codes.
All error codes are available through the TypeScript enumeration Code
.
Note that a code is an integer value, but can easily be converted to and
from a string value.
import { Code } from "@connectrpc/connect";
let code = Code.InvalidArgument;
code; // 3
let name = Code[code]; // "InvalidArgument"
let val: Code = Code["InvalidArgument"]; // 3
Error messages
The message
property contains a descriptive error message. In most cases,
the message is provided by the backend implementing the service. Because
message
is the only property that shows up in the browser console for
uncaught errors, the error message is always prefixed with the error code.
In case you do want the original error message without a code prefix, use
the property rawMessage
.
err.message; // "[invalid_argument] sentence cannot be empty"
if (err.code == Code.InvalidArgument) {
err.rawMessage; // "sentence cannot be empty"
}
Metadata
If you catch an error, your program takes an exception from the regular code
path, but you might still want to access a header or trailer value. Connect
provides a union of header and trailer values in the metadata
property as a
simple Headers
object:
err.metadata.get("Custom-Header-Value");
err.metadata.get("Custom-Trailer-Value");
Error details
On the wire, error details are wrapped with google.protobuf.Any
, so that a
server or middleware can attach arbitrary data to an error. Using the method
findDetails()
, you can decode the details from an error.
The method takes a protobuf message type as an argument, and returns an array of decoded messages of this type.
This example looks up a localized error message in the users preferred language:
import { ConnectError } from "@connectrpc/connect";
import { LocalizedMessage } from "./error_details_pb.js";
function handle(err: ConnectError) {
const localized = err.findDetails(LocalizedMessage)
.find(i => i.locale === navigator.language);
console.log(localized?.message);
}
We are using the protobuf message google.rpc.LocalizedMessage
in this example, but any protobuf message can be transmitted as error details.
Alternatively, findDetails()
takes a registry as an argument. See the
protobuf-es documentation
for details.
Working with errors
The method of handling errors will depend on the type of client being used.
Promise Clients
Promise-based clients can use a try-catch block to catch the error. Note that you must
verify it is a ConnectError
first.
import { ConnectError } from "@connectrpc/connect";
try {
await client.say({sentence: ""});
} catch (err) {
// We have to verify err is a ConnectError
// before using it as one.
if (err instanceof ConnectError) {
err.code; // Code.InvalidArgument
err.message; // "[invalid_argument] sentence cannot be empty"
}
// Alternatively, we can use ConnectError.from()
// It returns a ConnectError as is, and converts any
// other error to a ConnectError.
const connectErr = ConnectError.from(err);
connectErr.code; // Code.InvalidArgument
connectErr.message; // "[invalid_argument] sentence cannot be empty"
}
Callback Clients
Callback-based clients can use an error callback to receive the error.
import { ConnectError } from "@connectrpc/connect";
client.say({ sentence: "" },
(response) => {
// handle successful response
},
(err: ConnectError | undefined) => {
if (err) {
err.code; // Code.InvalidArgument
err.message; // "[invalid_argument] sentence cannot be empty"
}
},
);
Cancellation
There may be cases where you want to cancel a call, for example because the
user closed a dialog. All client methods take an optional AbortSignal
as an argument for this purpose. If this signal is aborted, the browser stops
the request, and the fetch API raises an AbortError
to abort the code path
of the application.
Connect converts those errors into a ConnectError
with code Canceled
,
so that you can handle cancellation appropriately:
import { createPromiseClient, ConnectError, Code } from "@connectrpc/connect";
const client = createPromiseClient(ElizaService, ...);
// For the sake of this example, let's cancel the request right away
const abort = new AbortController();
abort.abort();
try {
await client.say({sentence: "I feel..."}, { signal: abort.signal });
} catch (err) {
if (err instanceof ConnectError && err.code != Code.Canceled) {
// handle the genuine error, ignoring cancelled requests
}
}
Timeouts
Similar to the signal
option for cancellation, there is also the timeoutMs
option, which
allows you to specify a timeout, in milliseconds, for an individual call in instances
where the request is hung up or not responding. The timeout value is specified in the same
options object described above.
When a timeout is reached before the request has been completed, a ConnectError
with code DeadlineExceeded
will
be thrown.
An example using a timeout of 3 seconds:
try {
await client.say({sentence: "Hello"}, { timeoutMs: 3000 });
} catch (err) {
if (err instanceof ConnectError && err.code === Code.DeadlineExceeded) {
// handle the timeout error
}
}
Timeouts in Connect are the equivalent of deadlines in gRPC. They both behave similarly in that they will throw a "deadline exceeded" error. Since Connect's defined error codes map to the same codes and semantics as gRPC, this allows for easy interop between the two protocols.
There is no silver bullet to error handling with async/await and cancellation. In general, we recommend to let exception travel up to a central error handler in your application, and only catch errors in the case they need to be explicitly handled in your business logic.
But your use case may vary, and we encourage you to roll your own client if you want to represent errors and cancellation differently.