{"id":"d02d5a54745e40f2","repo":"redis/node-redis","slug":"all-replies-must-be-numbers-for-sum-aggregation","errorCode":null,"errorMessage":"All replies must be numbers for sum aggregation","messagePattern":"All replies must be numbers for sum aggregation","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/client/lib/cluster/request-response-policies/generic-aggregators.ts","lineNumber":155,"sourceCode":"    return aggregateElementwise(replies, Math.max, 'max') as T;\n  }\n  if (!replies.every((reply): reply is number => typeof reply === 'number')) {\n    throw new Error('All replies must be numbers for max aggregation');\n  }\n  return Math.max(...replies) as T;\n};\n\n/**\n * Aggregates multiple numbers by finding the sum of all values.\n * @remarks\n * This implementation is specifically designed for Array<number> type only,\n * despite the generic type parameter. The generic type parameter T is provided\n * for usage ergonomy, but the actual input structure will be validated at runtime.\n */\nexport const aggregateSum = <T>(replies: Array<unknown>): T => {\n  if (replies.length === 0) return 0 as T;\n  if (!replies.every((reply): reply is number => typeof reply === 'number')) {\n    throw new Error('All replies must be numbers for sum aggregation');\n  }\n  return replies.reduce((acc, reply) => acc + reply, 0) as T;\n};\n\n\nexport const aggregateMerge = <T>(replies: Array<unknown>): T => {\n\tif(replies.length === 0) return undefined as T;\n\n\tconst firstReply = replies[0]\n\n\tif(Array.isArray(firstReply)) {\n\t\tconst set = new Set()\n\t\tfor(const reply of replies) {\n\t\t\tfor(const item of reply as Array<unknown>) {\n\t\t\t\tset.add(item);\n\t\t\t}\n\t\t}\n\t\treturn Array.from(set) as T;","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/redis/node-redis/blob/bb5beb56578573910e2ee8f39681edc214c41398/packages/client/lib/cluster/request-response-policies/generic-aggregators.ts#L137-L173","documentation":"The `agg_sum` reducer requires every shard reply to be a number before summing. If any reply is non-numeric (string, null, object), it refuses to aggregate. Used by multi_shard commands like DEL/UNLINK/EXISTS/TOUCH (count of affected keys summed across slots). The dispatcher strips NUMBER typeMapping for numeric aggregators, so the cause is a genuine shape mismatch, not a String-mapped number.","triggerScenarios":"A multi_shard command (DEL, UNLINK, EXISTS, TOUCH) or any agg_sum-tagged command where at least one shard's reply is not a number — e.g. a node returned an error reply that wasn't a rejection, or a version where the integer reply changed type.","commonSituations":"A shard returning an error string instead of an integer count; RESP version mismatch; a module replying with a different type on some node; running against an inhomogeneous cluster during a rolling upgrade.","solutions":["Run DEL/UNLINK/EXISTS/TOUCH with keys hashing to a single slot (hash tags) to isolate which shard misbehaves.","Verify all nodes run the same Redis major/minor.","Check cluster health — a failing node can return error replies that surface here."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function allNumbers(replies: unknown[]): replies is number[] {\n  return replies.every(r => typeof r === 'number');\n}","tryCatchPattern":"try {\n  await cluster.del('{a}1', '{b}2', '{c}3');\n} catch (e) {\n  if (/must be numbers for sum/.test(e.message)) {\n    // a shard returned a non-integer count — check node health/version\n  } else throw e;\n}","preventionTips":["Use hash tags to isolate multi_shard commands to one slot when diagnosing shape errors.","Keep Redis versions uniform across the cluster during rolling upgrades.","Monitor node health — error replies from a failing node can surface as non-number counts."],"tags":["cluster","aggregation","response-policy","multi-shard","reply-shape"],"analyzedSha":"bb5beb56578573910e2ee8f39681edc214c41398","analyzedAt":"2026-08-03T19:09:15.686Z","schemaVersion":2}