Skip to content

Advanced Acknowledgments

Acknowledgments (ACKs) in Socket.IO enable implementing request-response patterns over bidirectional communication. This guide covers advanced acknowledgment usage.

Table of Contents

Fundamental Concepts

What are Acknowledgments?

Acknowledgments are callbacks that allow the sender to receive a response from the receiver:

// Client emits and waits for response
client.EmitWithAck("get-data", func(response ...interface{}) {
  fmt.Println("Server responded:", response[0])
}, "param1", "param2")

Acknowledgment Direction

ACKs can flow in both directions:

Client → Server → Client:

// Client requests data
client.EmitWithAck("get-user", func(response ...interface{}) {
  user := response[0].(map[string]interface{})
  fmt.Println("User:", user["name"])
}, "user-123")

Server → Client → Server:

// Server requests confirmation, client responds
client.On("confirm-action", func(data ...interface{}) {
  action := data[0].(string)

  // Last argument is the ACK callback
  if ack, ok := data[len(data)-1].(func(...interface{})); ok {
    // Confirm the action
    ack(map[string]interface{}{
      "confirmed": true,
      "timestamp": time.Now().Unix(),
    })
  }
})

Usage Patterns

Pattern: Simple Request-Response

Basic request with response:

client.EmitWithAck("get-data", func(response ...interface{}) {
  if response == nil {
    fmt.Println("Timeout: no response received")
    return
  }

  data := response[0]
  fmt.Printf("Data received: %v\n", data)
}, "data-id-123")

Pattern: Data Validation

Send data and validate it was processed correctly:

func saveData(client *socketio.Socket, data map[string]interface{}) error {
  var result error
  done := make(chan struct{})

  client.EmitWithAck("save-data", func(response ...interface{}) {
    defer close(done)

    if response == nil {
      result = fmt.Errorf("timeout waiting for confirmation")
      return
    }

    resp := response[0].(map[string]interface{})
    if resp["success"].(bool) {
      fmt.Println("Data saved successfully")
    } else {
      result = fmt.Errorf("error: %s", resp["error"])
    }
  }, data)

  <-done
  return result
}

// Usage
err := saveData(client, map[string]interface{}{
  "name": "Juan",
  "age": 30,
})
if err != nil {
  fmt.Println("Error:", err)
}

Pattern: Multiple Parallel Requests

Execute multiple requests in parallel and wait for all responses:

func fetchMultipleUsers(client *socketio.Socket, ids []string) map[string]interface{} {
  results := make(map[string]interface{})
  var mu sync.Mutex
  var wg sync.WaitGroup

  for _, id := range ids {
    wg.Add(1)
    go func(userId string) {
      defer wg.Done()

      client.EmitWithAck("get-user", func(response ...interface{}) {
        if response != nil {
          mu.Lock()
          results[userId] = response[0]
          mu.Unlock()
        }
      }, userId)
    }(id)
  }

  wg.Wait()
  return results
}

// Usage
users := fetchMultipleUsers(client, []string{"user-1", "user-2", "user-3"})
fmt.Printf("Fetched %d users\n", len(users))

Pattern: Request with Retry

Retry the request if it fails or times out:

func requestWithRetry(client *socketio.Socket, event string, maxRetries int, data ...interface{}) (interface{}, error) {
  for attempt := 0; attempt <= maxRetries; attempt++ {
    resultChan := make(chan interface{}, 1)
    errorChan := make(chan error, 1)

    client.EmitWithAck(event, func(response ...interface{}) {
      if response == nil {
        errorChan <- fmt.Errorf("timeout")
      } else {
        resultChan <- response[0]
      }
    }, data...)

    select {
    case result := <-resultChan:
      return result, nil
    case err := <-errorChan:
      if attempt < maxRetries {
        fmt.Printf("Attempt %d failed: %v. Retrying...\n", attempt+1, err)
        time.Sleep(time.Second * time.Duration(attempt+1)) // Exponential backoff
      } else {
        return nil, fmt.Errorf("failed after %d attempts: %v", maxRetries+1, err)
      }
    }
  }

  return nil, fmt.Errorf("failed after %d attempts", maxRetries+1)
}

// Usage
result, err := requestWithRetry(client, "get-data", 3, "param1", "param2")
if err != nil {
  fmt.Println("Error:", err)
} else {
  fmt.Println("Result:", result)
}

Pattern: Request Queue

Priority request queue:

type Request struct {
  Event    string
  Data     []interface{}
  Callback func(...interface{})
  Priority int
}

type RequestQueue struct {
  client   *socketio.Socket
  queue    []*Request
  mu       sync.Mutex
  running  bool
}

func NewRequestQueue(client *socketio.Socket) *RequestQueue {
  return &RequestQueue{
    client: client,
    queue:  make([]*Request, 0),
  }
}

func (rq *RequestQueue) Add(req *Request) {
  rq.mu.Lock()
  defer rq.mu.Unlock()

  rq.queue = append(rq.queue, req)

  // Sort by priority (highest first)
  sort.Slice(rq.queue, func(i, j int) bool {
    return rq.queue[i].Priority > rq.queue[j].Priority
  })

  if !rq.running {
    go rq.process()
  }
}

func (rq *RequestQueue) process() {
  rq.running = true
  defer func() { rq.running = false }()

  for {
    rq.mu.Lock()
    if len(rq.queue) == 0 {
      rq.mu.Unlock()
      return
    }

    req := rq.queue[0]
    rq.queue = rq.queue[1:]
    rq.mu.Unlock()

    // Execute request
    rq.client.EmitWithAck(req.Event, req.Callback, req.Data...)

    // Small delay between requests
    time.Sleep(50 * time.Millisecond)
  }
}

// Usage
queue := NewRequestQueue(client)

// High priority request
queue.Add(&Request{
  Event:    "urgent-data",
  Data:     []interface{}{"id-1"},
  Priority: 10,
  Callback: func(response ...interface{}) {
    fmt.Println("Urgent response:", response[0])
  },
})

// Normal priority request
queue.Add(&Request{
  Event:    "normal-data",
  Data:     []interface{}{"id-2"},
  Priority: 5,
  Callback: func(response ...interface{}) {
    fmt.Println("Normal response:", response[0])
  },
})

Timeout Handling

Global Timeout

Configure global timeout for all acknowledgments:

client := socketio.New("ws://localhost:3000", socketio.Options{
  AckTimeout: 10 * time.Second, // 10 second timeout
})

client.EmitWithAck("slow-operation", func(response ...interface{}) {
  if response == nil {
    fmt.Println("⏱️  Operation took longer than 10 seconds")
    return
  }
  fmt.Println("Completed:", response[0])
}, "data")

Per-Request Timeout

Implement custom timeout per request:

func emitWithCustomTimeout(client *socketio.Socket, event string, timeout time.Duration, data ...interface{}) (interface{}, error) {
  resultChan := make(chan interface{}, 1)
  timeoutChan := time.After(timeout)

  client.EmitWithAck(event, func(response ...interface{}) {
    if response != nil {
      resultChan <- response[0]
    }
  }, data...)

  select {
  case result := <-resultChan:
    return result, nil
  case <-timeoutChan:
    return nil, fmt.Errorf("timeout after %v", timeout)
  }
}

// Usage with short timeout
result, err := emitWithCustomTimeout(client, "quick-op", 2*time.Second, "data")
if err != nil {
  fmt.Println("Error:", err)
}

// Usage with long timeout
result, err = emitWithCustomTimeout(client, "slow-op", 30*time.Second, "data")
if err != nil {
  fmt.Println("Error:", err)
}

Timeout Detection

Distinguish between timeout and server error:

client.EmitWithAck("operation", func(response ...interface{}) {
  if response == nil {
    // Timeout: server did not respond
    fmt.Println("⏱️  Timeout: server did not respond in time")
    handleTimeout()
    return
  }

  // Response received, check if it's an error
  if resp, ok := response[0].(map[string]interface{}); ok {
    if errorMsg, hasError := resp["error"]; hasError {
      // Server error
      fmt.Printf("❌ Server error: %s\n", errorMsg)
      handleServerError(errorMsg.(string))
      return
    }

    // Success
    fmt.Println("✅ Operation successful:", resp["data"])
    handleSuccess(resp["data"])
  }
}, "data")

Error Handling

Pattern: Standard Error Response

Define standard format for error responses:

type AckResponse struct {
  Success bool                   `json:"success"`
  Data    interface{}            `json:"data,omitempty"`
  Error   string                 `json:"error,omitempty"`
  Code    int                    `json:"code,omitempty"`
}

func handleAckResponse(response ...interface{}) (*AckResponse, error) {
  if response == nil {
    return nil, fmt.Errorf("timeout: no response from server")
  }

  if len(response) == 0 {
    return nil, fmt.Errorf("empty response")
  }

  respMap, ok := response[0].(map[string]interface{})
  if !ok {
    return nil, fmt.Errorf("invalid response format")
  }

  ackResp := &AckResponse{}

  if success, ok := respMap["success"].(bool); ok {
    ackResp.Success = success
  }

  if data, ok := respMap["data"]; ok {
    ackResp.Data = data
  }

  if errorMsg, ok := respMap["error"].(string); ok {
    ackResp.Error = errorMsg
  }

  if code, ok := respMap["code"].(float64); ok {
    ackResp.Code = int(code)
  }

  if !ackResp.Success && ackResp.Error != "" {
    return ackResp, fmt.Errorf("error %d: %s", ackResp.Code, ackResp.Error)
  }

  return ackResp, nil
}

// Usage
client.EmitWithAck("operation", func(response ...interface{}) {
  resp, err := handleAckResponse(response...)
  if err != nil {
    fmt.Println("Error:", err)
    return
  }

  fmt.Println("Data:", resp.Data)
}, "params")

Pattern: Graceful Degradation

Handle failures gracefully with fallback values:

func getUserDataWithDefault(client *socketio.Socket, userId string) map[string]interface{} {
  defaultUser := map[string]interface{}{
    "id": userId,
    "name": "Unknown User",
    "status": "offline",
  }

  resultChan := make(chan map[string]interface{}, 1)

  client.EmitWithAck("get-user", func(response ...interface{}) {
    if response == nil {
      // Timeout: use default values
      resultChan <- defaultUser
      return
    }

    if user, ok := response[0].(map[string]interface{}); ok {
      resultChan <- user
    } else {
      resultChan <- defaultUser
    }
  }, userId)

  // Wait with additional timeout
  select {
  case user := <-resultChan:
    return user
  case <-time.After(6 * time.Second):
    // Additional timeout: return default values
    return defaultUser
  }
}

// Usage
user := getUserDataWithDefault(client, "user-123")
fmt.Printf("User: %s (%s)\n", user["name"], user["status"])

Advanced Patterns

Pattern: Promise-like API

Wrap acknowledgments in a Promise-like API:

type Promise struct {
  done   chan struct{}
  result interface{}
  err    error
}

func NewPromise(client *socketio.Socket, event string, data ...interface{}) *Promise {
  p := &Promise{
    done: make(chan struct{}),
  }

  client.EmitWithAck(event, func(response ...interface{}) {
    defer close(p.done)

    if response == nil {
      p.err = fmt.Errorf("timeout")
      return
    }

    if resp, ok := response[0].(map[string]interface{}); ok {
      if errMsg, hasError := resp["error"]; hasError {
        p.err = fmt.Errorf("%s", errMsg)
        return
      }
      p.result = resp["data"]
    } else {
      p.result = response[0]
    }
  }, data...)

  return p
}

func (p *Promise) Then(callback func(interface{})) *Promise {
  go func() {
    <-p.done
    if p.err == nil {
      callback(p.result)
    }
  }()
  return p
}

func (p *Promise) Catch(callback func(error)) *Promise {
  go func() {
    <-p.done
    if p.err != nil {
      callback(p.err)
    }
  }()
  return p
}

func (p *Promise) Await() (interface{}, error) {
  <-p.done
  return p.result, p.err
}

// Promise-style usage
NewPromise(client, "get-data", "id-123").
  Then(func(data interface{}) {
    fmt.Println("Data:", data)
  }).
  Catch(func(err error) {
    fmt.Println("Error:", err)
  })

// Await-style usage
data, err := NewPromise(client, "get-data", "id-123").Await()
if err != nil {
  fmt.Println("Error:", err)
} else {
  fmt.Println("Data:", data)
}

Pattern: Batch Requests

Send multiple requests in a single acknowledgment:

type BatchRequest struct {
  ID   string        `json:"id"`
  Data interface{}   `json:"data"`
}

type BatchResponse struct {
  ID     string      `json:"id"`
  Result interface{} `json:"result"`
  Error  string      `json:"error,omitempty"`
}

func batchRequest(client *socketio.Socket, requests []BatchRequest) (map[string]interface{}, error) {
  results := make(map[string]interface{})
  done := make(chan struct{})

  client.EmitWithAck("batch", func(response ...interface{}) {
    defer close(done)

    if response == nil {
      return
    }

    if responses, ok := response[0].([]interface{}); ok {
      for _, r := range responses {
        resp := r.(map[string]interface{})
        id := resp["id"].(string)

        if errMsg, hasError := resp["error"]; hasError && errMsg != "" {
          results[id] = fmt.Errorf("%s", errMsg)
        } else {
          results[id] = resp["result"]
        }
      }
    }
  }, requests)

  <-done

  if len(results) == 0 {
    return nil, fmt.Errorf("timeout or no responses")
  }

  return results, nil
}

// Usage
requests := []BatchRequest{
  {ID: "req-1", Data: "user-1"},
  {ID: "req-2", Data: "user-2"},
  {ID: "req-3", Data: "user-3"},
}

results, err := batchRequest(client, requests)
if err != nil {
  fmt.Println("Error:", err)
  return
}

for id, result := range results {
  if err, isError := result.(error); isError {
    fmt.Printf("%s: Error - %v\n", id, err)
  } else {
    fmt.Printf("%s: Success - %v\n", id, result)
  }
}

Pattern: Streaming with Acknowledgments

Implement data streaming with incremental confirmations:

func streamDataWithAcks(client *socketio.Socket, chunks [][]byte) error {
  for i, chunk := range chunks {
    done := make(chan error, 1)

    client.EmitBinaryWithAck("stream-chunk", func(response ...interface{}) {
      if response == nil {
        done <- fmt.Errorf("timeout on chunk %d", i)
        return
      }

      resp := response[0].(map[string]interface{})
      if resp["success"].(bool) {
        done <- nil
      } else {
        done <- fmt.Errorf("error on chunk %d: %s", i, resp["error"])
      }
    }, chunk, map[string]interface{}{
      "index": i,
      "total": len(chunks),
      "last": i == len(chunks)-1,
    })

    if err := <-done; err != nil {
      return err
    }

    fmt.Printf("Chunk %d/%d sent\n", i+1, len(chunks))
  }

  return nil
}

// Usage
data, _ := os.ReadFile("large-file.bin")
chunks := splitIntoChunks(data, 1024*64) // 64KB chunks

if err := streamDataWithAcks(client, chunks); err != nil {
  fmt.Println("Streaming error:", err)
} else {
  fmt.Println("File sent completely")
}

Best Practices

1. Always Check for nil

// ✅ Good
client.EmitWithAck("event", func(response ...interface{}) {
  if response == nil {
    fmt.Println("Timeout")
    return
  }
  // Process response
}, data)

// ❌ Bad
client.EmitWithAck("event", func(response ...interface{}) {
  data := response[0] // Panic if response == nil!
}, data)

2. Use Safe Type Assertions

// ✅ Good
if user, ok := response[0].(map[string]interface{}); ok {
  fmt.Println(user["name"])
} else {
  fmt.Println("Unexpected format")
}

// ❌ Bad
user := response[0].(map[string]interface{}) // Panic if wrong type!

3. Configure Appropriate Timeouts

// ✅ Good: timeout based on operation type
fastOps := socketio.Options{AckTimeout: 2 * time.Second}
slowOps := socketio.Options{AckTimeout: 30 * time.Second}

// ❌ Bad: same timeout for everything
opts := socketio.Options{AckTimeout: 5 * time.Second} // May be too short or too long

4. Document Response Format

// ✅ Good: document expected response
// Expected response: {success: bool, data: User, error: string}
client.EmitWithAck("get-user", func(response ...interface{}) {
  // ...
}, userId)

See Also