26 Handling Errors and Context in Streaming RPC
In the era of cloud native architecture, Remote Procedure Call (RPC) has evolved from a simple request-response pattern into a sophisticated communication architecture, one of which is streaming RPC. However, the more complex the architecture, the more it demands that developers understand how to handle errors and context properly. In this article, I’ll dig into the techniques for handling errors and context in streaming RPC, especially using gRPC, and wrap them up with case studies, code examples, simulations, and flow diagrams to make the concepts clearer.
What Is Streaming RPC?
At its core, RPC is a technique that lets an application call a function/procedure that runs on a different service (usually over a network). In streaming RPC, the client, the server, or both can send a sequence of data (a stream) rather than just a single request and a single response.
There are four types of RPC in gRPC:
| Type | Client Stream | Server Stream | Example |
|---|---|---|---|
| Unary | No | No | Login(username, password) |
| Client-side Streaming | Yes | No | Uploading logs in batches |
| Server-side Streaming | No | Yes | Status update notifications |
| Bidirectional Streaming | Yes | Yes | Chat, game lobby, live data |
Why Are Errors and Context Important in Streaming RPC?
Simply put, streaming RPC runs for a long time or continuously — for example, a client sending thousands of logs to the server incrementally. During that process, many things can happen:
- The client suddenly disconnects.
- The server gets overloaded and crashes.
- A timeout occurs because the network drops.
- The client wants to cancel the process.
- The server hits a fatal error and has to terminate the stream.
Ignoring error handling and context management on a stream makes the system fragile, hard to maintain, or even prone to memory leaks.
How to Handle Errors in Streaming RPC
1. Recognizing gRPC Errors
gRPC has a status code system similar to HTTP (though the terminology differs). Here are some common codes you’ll frequently run into:
| Status Code | Explanation | Common Solution |
|---|---|---|
OK | No error (success) | — |
Canceled | The client canceled the request | Clean up resources |
DeadlineExceeded | Timeout on the stream | Review context/timeout |
Unavailable | Server unavailable (down) | Try reconnect/backoff |
Internal | Internal error on the server | Check the server logs |
DataLoss | Stream data is corrupt | Validate, retry |
2. Handling Errors in Client Streaming
Suppose a client streams data to the server incrementally. On the Go client side, we typically see this pattern:
1stream, err := client.SendLogStream(ctx)
2if err != nil {
3 log.Fatalf("failed to open stream: %v", err)
4}
5
6for _, entry := range logs {
7 if err := stream.Send(entry); err != nil {
8 log.Printf("failed to send log: %v", err)
9 break
10 }
11}
12
13resp, err := stream.CloseAndRecv()
14if err != nil {
15 log.Fatalf("failed to get response: %v", err)
16}
17log.Printf("Upload succeeded: %v", resp.Total)What If the Server Returns an Error During the Process?
When stream.Send() fails, the client must check: has the context already been canceled? Is the error because the server closed the connection first?
3. Error Handling in Server Streaming
On the server side, you’ll typically send data to the client periodically. At some point the client may disconnect, causing the server’s Send() to return an error.
1func (s *server) StreamNotifikasi(req *pb.Request, stream pb.Service_StreamNotifikasiServer) error {
2 for notif := range notifikasiCh {
3 if err := stream.Send(notif); err != nil {
4 // Usually context canceled or deadline exceeded
5 log.Printf("failed to send notification: %v", err)
6 return err
7 }
8 }
9 return nil
10}Note: The server MUST handle send errors to avoid goroutine leaks or resource leaks.
Managing Context in Streaming RPC
What Is Context in gRPC?
context.Context is a crucial object in Go/gRPC for carrying timeout, cancellation, and deadline throughout the request lifecycle (including streams). This context becomes the primary controller for canceling a stream, stopping resources, and halting the streaming process.
Wiring Context into a Stream
Every stream in gRPC is bound to the same context as the main request.
- On the client side, the context is usually provided when the stream is created.
- On the server side, gRPC automatically supplies a context per stream (accessible via
stream.Context()).
Example of Using Context on the Client
1ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
2defer cancel()
3
4stream, err := client.SendLogStream(ctx)
5// ...Capturing Context on the Server
1for {
2 select {
3 case <-stream.Context().Done():
4 // Client disconnected or canceled
5 log.Println("stream canceled by the client")
6 return stream.Context().Err()
7 case notif := <-notifikasiCh:
8 if err := stream.Send(notif); err != nil {
9 // Send failed (e.g. client disconnected)
10 return err
11 }
12 }
13}Flow Diagram: Errors and Context in Server Streaming
flowchart TD
A[Mulai Stream] --> B{Kirim Notifikasi}
B -->|Notifikasi Baru| C["stream.Send()"]
C --> D{Error saat Send?}
D -->|Ya| E["Periksa stream.Context().Err()"]
D -->|Tidak| B
E -->|Err Canceled/Deadline| F[Selesaikan stream]
E -->|Error lain| G[Log error & return]
Case Study: Uploading a File with Client Streaming
Imagine we want to implement a feature for uploading large files (say, a log file) from the client to the server in chunks (streaming). The problem: the network is unstable, the client sometimes cancels midway, and the server gets overloaded.
1. Protobuf Definition
1service LogUploader {
2 rpc UploadLogs(stream LogChunk) returns (UploadSummary);
3}
4message LogChunk {
5 bytes content = 1;
6}
7message UploadSummary {
8 int64 total = 1;
9}2. Server Implementation: Error and Context Handling
1func (s *server) UploadLogs(stream pb.LogUploader_UploadLogsServer) error {
2 total := int64(0)
3 for {
4 select {
5 case <-stream.Context().Done():
6 // Client disconnect/timeout/cancel
7 log.Printf("stream canceled by the client: %v", stream.Context().Err())
8 return status.Error(codes.Canceled, "client canceled the upload")
9 default:
10 chunk, err := stream.Recv()
11 if err == io.EOF {
12 return stream.SendAndClose(&pb.UploadSummary{Total: total})
13 }
14 if err != nil {
15 log.Printf("error reading stream: %v", err)
16 return status.Error(codes.Internal, "failed to read data")
17 }
18 // Simulated error: chunk too large
19 if len(chunk.Content) > MAX_CHUNK {
20 return status.Error(codes.InvalidArgument, "chunk too large")
21 }
22 total += int64(len(chunk.Content))
23 }
24 }
25}3. Simulation: Case – Client Cancels the Upload
If the client calls cancel() on the context:
- The server operation on
stream.Context().Done()will be triggered. - The server can immediately clean up resources and exit.
- The client will receive a
Cancelederror.
| Step | Client | Server |
|---|---|---|
| Start the stream | Opens the stream | Waits/receives via Recv |
| Upload some chunks | Sends chunks | Receives, stores, checks error |
| Cancel on the client | Cancels the context | Stream Context Done triggered |
| Stream closed | Receives Canceled error | Exits the handler |
Practical Recommendations for Engineers
- Always check for errors on every stream operation — both
Send()andRecv(). - Observe context on the server: Watch the
<-stream.Context().Done()channel for fast cleanup when the client disconnects or times out. - Handle status codes correctly: Return relevant errors (not just
Internal). Usestatus.Errorin Go. - Use timeouts as needed: Don’t let streaming operations run indefinitely.
- Test for resilience: Simulate various error conditions during development (client suddenly disconnecting, server overload, and so on).
Conclusion
Handling errors and context correctly in streaming RPC not only makes your application more robust, but also more efficient and easier to maintain in production. Remember, streams will always be prone to failure because they run “for a long time” and pass through many edge cases. A good engineer doesn’t just make features work; they’re able to handle every possible error gracefully.
Happy coding, and don’t forget to refactor! 🚀