|
| 1 | +# Errors |
| 2 | + |
| 3 | +Similar to the familiar "404 Not Found" and "500 Internal Server Error" status codes in HTTP, Connect uses a set of [16 error codes](https://connectrpc.com/docs/protocol#error-codes). These error codes are designed to work consistently across Connect, gRPC, and gRPC-Web protocols. |
| 4 | + |
| 5 | +## Working with errors |
| 6 | + |
| 7 | +Connect handlers raise errors using `ConnectError`: |
| 8 | + |
| 9 | +=== "ASGI" |
| 10 | + |
| 11 | + ```python |
| 12 | + from connectrpc.code import Code |
| 13 | + from connectrpc.errors import ConnectError |
| 14 | + from connectrpc.request import RequestContext |
| 15 | + |
| 16 | + async def greet(self, request: GreetRequest, ctx: RequestContext) -> GreetResponse: |
| 17 | + if not request.name: |
| 18 | + raise ConnectError(Code.INVALID_ARGUMENT, "name is required") |
| 19 | + return GreetResponse(greeting=f"Hello, {request.name}!") |
| 20 | + ``` |
| 21 | + |
| 22 | +=== "WSGI" |
| 23 | + |
| 24 | + ```python |
| 25 | + from connectrpc.code import Code |
| 26 | + from connectrpc.errors import ConnectError |
| 27 | + from connectrpc.request import RequestContext |
| 28 | + |
| 29 | + def greet(self, request: GreetRequest, ctx: RequestContext) -> GreetResponse: |
| 30 | + if not request.name: |
| 31 | + raise ConnectError(Code.INVALID_ARGUMENT, "name is required") |
| 32 | + return GreetResponse(greeting=f"Hello, {request.name}!") |
| 33 | + ``` |
| 34 | + |
| 35 | +Clients catch errors the same way: |
| 36 | + |
| 37 | +=== "Async" |
| 38 | + |
| 39 | + ```python |
| 40 | + from connectrpc.code import Code |
| 41 | + from connectrpc.errors import ConnectError |
| 42 | + |
| 43 | + async with GreetServiceClient("http://localhost:8000") as client: |
| 44 | + try: |
| 45 | + response = await client.greet(GreetRequest(name="")) |
| 46 | + except ConnectError as e: |
| 47 | + if e.code == Code.INVALID_ARGUMENT: |
| 48 | + print(f"Invalid request: {e.message}") |
| 49 | + else: |
| 50 | + print(f"RPC failed: {e.code} - {e.message}") |
| 51 | + ``` |
| 52 | + |
| 53 | +=== "Sync" |
| 54 | + |
| 55 | + ```python |
| 56 | + from connectrpc.code import Code |
| 57 | + from connectrpc.errors import ConnectError |
| 58 | + |
| 59 | + with GreetServiceClientSync("http://localhost:8000") as client: |
| 60 | + try: |
| 61 | + response = client.greet(GreetRequest(name="")) |
| 62 | + except ConnectError as e: |
| 63 | + if e.code == Code.INVALID_ARGUMENT: |
| 64 | + print(f"Invalid request: {e.message}") |
| 65 | + else: |
| 66 | + print(f"RPC failed: {e.code} - {e.message}") |
| 67 | + ``` |
| 68 | + |
| 69 | +## Error codes |
| 70 | + |
| 71 | +Connect uses a set of [16 error codes](https://connectrpc.com/docs/protocol#error-codes). The `code` property of a `ConnectError` holds one of these codes. All error codes are available through the `Code` enumeration: |
| 72 | + |
| 73 | +```python |
| 74 | +from connectrpc.code import Code |
| 75 | + |
| 76 | +code = Code.INVALID_ARGUMENT |
| 77 | +code.value # "invalid_argument" |
| 78 | + |
| 79 | +# Access by name |
| 80 | +Code["INVALID_ARGUMENT"] # Code.INVALID_ARGUMENT |
| 81 | +``` |
| 82 | + |
| 83 | +## Error messages |
| 84 | + |
| 85 | +The `message` property contains a descriptive error message. In most cases, the message is provided by the backend implementing the service: |
| 86 | + |
| 87 | +```python |
| 88 | +try: |
| 89 | + response = await client.greet(GreetRequest(name="")) |
| 90 | +except ConnectError as e: |
| 91 | + print(e.message) # "name is required" |
| 92 | +``` |
| 93 | + |
| 94 | +## Error details |
| 95 | + |
| 96 | +Errors can include strongly-typed details using protobuf messages: |
| 97 | + |
| 98 | +```python |
| 99 | +from connectrpc.code import Code |
| 100 | +from connectrpc.errors import ConnectError |
| 101 | +from connectrpc.request import RequestContext |
| 102 | +from google.protobuf.struct_pb2 import Struct, Value |
| 103 | + |
| 104 | +async def create_user(self, request: CreateUserRequest, ctx: RequestContext) -> CreateUserResponse: |
| 105 | + if not request.email: |
| 106 | + error_detail = Struct(fields={ |
| 107 | + "field": Value(string_value="email"), |
| 108 | + "issue": Value(string_value="Email is required") |
| 109 | + }) |
| 110 | + |
| 111 | + raise ConnectError( |
| 112 | + Code.INVALID_ARGUMENT, |
| 113 | + "Invalid user request", |
| 114 | + details=[error_detail] |
| 115 | + ) |
| 116 | + # ... rest of implementation |
| 117 | +``` |
| 118 | + |
| 119 | +### Reading error details on the client |
| 120 | + |
| 121 | +Error details are `google.protobuf.Any` messages that can be unpacked to their original types: |
| 122 | + |
| 123 | +```python |
| 124 | +try: |
| 125 | + response = await client.some_method(request) |
| 126 | +except ConnectError as e: |
| 127 | + for detail in e.details: |
| 128 | + # Check the type before unpacking |
| 129 | + if detail.Is(Struct.DESCRIPTOR): |
| 130 | + unpacked = Struct() |
| 131 | + detail.Unpack(unpacked) |
| 132 | + print(f"Error detail: {unpacked}") |
| 133 | +``` |
| 134 | + |
| 135 | +### Standard error detail types |
| 136 | + |
| 137 | +With `googleapis-common-protos` installed, you can use standard types like: |
| 138 | + |
| 139 | +- `BadRequest`: Field violations in a request |
| 140 | +- `RetryInfo`: When to retry |
| 141 | +- `Help`: Links to documentation |
| 142 | +- `QuotaFailure`: Quota violations |
| 143 | +- `ErrorInfo`: Structured error metadata |
| 144 | + |
| 145 | +Example: |
| 146 | + |
| 147 | +```python |
| 148 | +from google.rpc.error_details_pb2 import BadRequest |
| 149 | + |
| 150 | +bad_request = BadRequest() |
| 151 | +violation = bad_request.field_violations.add() |
| 152 | +violation.field = "email" |
| 153 | +violation.description = "Must be a valid email address" |
| 154 | + |
| 155 | +raise ConnectError( |
| 156 | + Code.INVALID_ARGUMENT, |
| 157 | + "Invalid email format", |
| 158 | + details=[bad_request] |
| 159 | +) |
| 160 | +``` |
| 161 | + |
| 162 | +## HTTP representation |
| 163 | + |
| 164 | +In the Connect protocol, errors are always JSON: |
| 165 | + |
| 166 | +```http |
| 167 | +HTTP/1.1 400 Bad Request |
| 168 | +Content-Type: application/json |
| 169 | +
|
| 170 | +{ |
| 171 | + "code": "invalid_argument", |
| 172 | + "message": "name is required", |
| 173 | + "details": [ |
| 174 | + { |
| 175 | + "type": "google.protobuf.Struct", |
| 176 | + "value": "base64-encoded-protobuf" |
| 177 | + } |
| 178 | + ] |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +The `details` array contains error detail messages, where each entry has: |
| 183 | + |
| 184 | +- `type`: The fully-qualified protobuf message type (e.g., `google.protobuf.Struct`) |
| 185 | +- `value`: The protobuf message serialized in binary format and then base64-encoded |
| 186 | + |
| 187 | +## See also |
| 188 | + |
| 189 | +- [Interceptors](interceptors.md) for error transformation and logging |
| 190 | +- [Streaming](streaming.md) for stream-specific error handling |
| 191 | +- [Headers and trailers](headers-and-trailers.md) for attaching metadata to errors |
| 192 | +- [Usage guide](usage.md) for error handling best practices |
0 commit comments