Errors
Failures normalise onto typefetch's shared ErrorKind, so one global handler
covers every transport:
client.onError((error) => {
if (error.kind === "unauthenticated") redirectToLogin();
});That fires for a gRPC UNAUTHENTICATED exactly as it does for an HTTP 401 and a
GraphQL UNAUTHENTICATED.
The typed error body stays gRPC-native: a gRPC endpoint's errors map is
keyed by the numeric gRPC code, not the HTTP status.
import { GrpcCode } from "@tahanabavi/typefetch-grpc";
getUser: {
transport: "grpc",
service: "user.v1.UserService",
rpc: "GetUser",
request: z.object({ id: z.string() }),
response: User,
errors: {
[GrpcCode.NotFound]: z.object({ code: z.string(), message: z.string() }),
},
}if (isContractError(contracts.user.getUser, e, GrpcCode.NotFound)) {
e.data.message; // typed from the schema above
}Keying on the code rather than the status is what makes a contract's declared
errors mean the same thing over Connect (which maps codes onto real statuses) and
over binary grpc-web (which answers 200 and reports the code in a trailer).
RichError.grpcCode carries the numeric code; RichError.status stays the HTTP
status.