Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
04 Jul 2025 · 7 min read ·Article 26 / 110
Go

26 Handling Errors and Context in Streaming RPC

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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:

TypeClient StreamServer StreamExample
UnaryNoNoLogin(username, password)
Client-side StreamingYesNoUploading logs in batches
Server-side StreamingNoYesStatus update notifications
Bidirectional StreamingYesYesChat, 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 CodeExplanationCommon Solution
OKNo error (success)
CanceledThe client canceled the requestClean up resources
DeadlineExceededTimeout on the streamReview context/timeout
UnavailableServer unavailable (down)Try reconnect/backoff
InternalInternal error on the serverCheck the server logs
DataLossStream data is corruptValidate, 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:

go
 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.

go
 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

go
1ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
2defer cancel()
3
4stream, err := client.SendLogStream(ctx)
5// ...
If the stream process exceeds 5 seconds, the context is automatically canceled. All operations on the stream will return an error of type “context canceled” or “deadline exceeded”.

Capturing Context on the Server

go
 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

MERMAID
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

protobuf
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

go
 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 Canceled error.
StepClientServer
Start the streamOpens the streamWaits/receives via Recv
Upload some chunksSends chunksReceives, stores, checks error
Cancel on the clientCancels the contextStream Context Done triggered
Stream closedReceives Canceled errorExits the handler

Practical Recommendations for Engineers

  1. Always check for errors on every stream operation — both Send() and Recv().
  2. Observe context on the server: Watch the <-stream.Context().Done() channel for fast cleanup when the client disconnects or times out.
  3. Handle status codes correctly: Return relevant errors (not just Internal). Use status.Error in Go.
  4. Use timeouts as needed: Don’t let streaming operations run indefinitely.
  5. 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! 🚀

Related Articles

💬 Comments