Learn gRPC - gRPC Unary, Streaming, and Basic Error Handling
Series/Learn gRPC/Episode 5
Episode 5 of 19

Learn gRPC - gRPC Unary, Streaming, and Basic Error Handling

This episode covers the real implementation of the four RPC patterns: unary, server streaming, client streaming, and bidirectional streaming. You'll also learn error handling with status codes, rich error details, and handling deadlines and cancellation on both sides.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

The contract has been generated and the first server is running. Now it's time to complete gRPC's core capabilities: the four RPC patterns and proper error handling. These two things separate a gRPC application that merely runs from one that's production-ready.

In episode 5 you'll implement unary, server streaming, client streaming, and bidirectional streaming in a single service. After that, we cover status codes, rich error details, and deadlines and cancellation — topics that will keep coming up through the end of the series.

Unary RPC

Unary is the one-request-one-response pattern. This is what you already wrote in episode 4, and it remains the most common pattern for operations like reading a single entity:

Unary: request-response
func (s *catalogServer) GetProduct(
    ctx context.Context, in *pb.ProductId,
) (*pb.Product, error) {
    if in.Id == "" {
        return nil, status.Error(codes.InvalidArgument, "id wajib diisi")
    }
    return s.db.Find(in.Id)
}

Notice status.Error(codes.InvalidArgument, "id wajib diisi"): the server returns a standard status code, not just a string. The client can read this code programmatically to make decisions.

Server Streaming

Server streaming sends many responses for a single request — perfect for large lists that aren't practical to send all at once:

Server streaming with a stream
func (s *catalogServer) ListProducts(
    in *pb.ProductQuery, stream pb.CatalogService_ListProductsServer,
) error {
    products, err := s.db.Search(in.Keyword)
    if err != nil {
        return err
    }
    for _, p := range products {
        if err := stream.Send(p); err != nil {
            return err
        }
    }
    return nil
}

The client reads results one by one via stream.Recv() until it gets io.EOF. The stream.Send(p) method streams one message per iteration without waiting for all the data to be collected — memory stays under control even for lists of millions of rows.

Client Streaming

Client streaming flips the direction: the client sends many messages, and the server replies with a single message at the end. Useful for uploads or batch aggregation:

Client streaming on the server side
func (s *catalogServer) AddBulk(
    stream pb.CatalogService_AddBulkServer,
) error {
    var count int32
    for {
        p, err := stream.Recv()
        if err == io.EOF {
            return stream.SendAndClose(&pb.BulkResult{AddedCount: count})
        }
        if err != nil {
            return err
        }
        s.db.Upsert(p)
        count++
    }
}

The loop ends when stream.Recv() == io.EOF, signaling that the client is done sending. Then the server returns a single summary via SendAndClose.

Bidirectional Streaming

The strongest pattern: both sides send and receive simultaneously. Ideal for chat, voice, or real-time stream processing:

Bidirectional streaming
func (s *catalogServer) Search(
    stream pb.CatalogService_SearchServer,
) error {
    for {
        term, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        for _, hit := range s.db.Search(term.Keyword) {
            if err := stream.Send(hit); err != nil {
                return err
            }
        }
    }
}

On the client, stream.Send and stream.Recv can be called concurrently from different goroutines. stream.Send(hit) inside the same loop is still safe because the flow is sequential within a single goroutine.

Error Handling and Deadlines

Status Codes and Rich Error Details

Status codes give a structured signal; rich error details add context. The gRPC standard uses google/rpc/error_details.proto:

Status with details
st := status.New(codes.OutOfRange, "kuota produk terlampaui")
ds, _ := st.WithDetails(
    &errdetails.QuotaFailure{Violations: []*errdetails.QuotaFailure_Violation{
        {Subject: "product:add", Description: "melebihi 1000 item"},
    }},
)
return nil, ds.Err()

The client reads status.FromError(err) then calls status.Convert(err) to extract the details. &errdetails.QuotaFailure{...} gives machines (and humans) context that can be parsed.

Handling Deadlines and Cancellation

On the client side, the deadline is set via context:

Deadline on the client
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()

On the server side, check for cancellation so work isn't continued in vain:

Check cancellation on the server
select {
case <-ctx.Done():
    return nil, status.FromContextError(ctx.Err()).Err()
default:
}

When the deadline is exceeded, the client receives DEADLINE_EXCEEDED and cancellation propagates through the entire call chain. status.FromContextError(ctx.Err()) translates the context error into the correct gRPC status code.

Closing

Key takeaways:

  • Unary for simple request-response; server streaming for large lists.
  • Client streaming for batch uploads; bidirectional for real-time two-way communication.
  • Status codes are the standard error language between client and server.
  • Rich error details add machine-parseable context.
  • Always set a deadline on the client and respect cancellation on the server.
  • io.EOF marks the end of a stream from the sender's side.

In episode 6 next, we cover metadata, interceptors, and lifecycle callbacks — how to send authentication tokens via metadata, build interceptors on the client and server side for both unary and streaming, and lifecycle callbacks for request logging, tracing, and authorization context. These patterns turn your error handling into reusable infrastructure.

Learn gRPC - gRPC Unary, Streaming, and Basic Error Handling | Learn gRPC