diff --git a/src/box/tuple_update.c b/src/box/tuple_update.c
index 7a203ced82cf298b2c052f87517e28bb283453ff..785a88640ce2ff3fb6fe04abcb0caf86a62da940 100644
--- a/src/box/tuple_update.c
+++ b/src/box/tuple_update.c
@@ -33,6 +33,7 @@
 
 #include <stdio.h>
 #include <stdbool.h>
+#include <float.h>
 
 #include "say.h"
 #include "error.h"
@@ -484,15 +485,15 @@ make_arith_operation(struct op_arith_arg arg1, struct op_arith_arg arg2,
 				 err_fieldno, "a positive integer");
 			return -1;
 		}
-		if (lowest_type == AT_DOUBLE) {
-			/* result is DOUBLE */
+		float fc = (float)c;
+		if ((lowest_type == AT_DOUBLE && c != (double) fc) ||
+		    (lowest_type == AT_FLOAT &&
+		     (c >= FLT_MAX || c <= -FLT_MAX))) {
 			ret->type = AT_DOUBLE;
 			ret->dbl = c;
 		} else {
-			/* result is FLOAT */
-			assert(lowest_type == AT_FLOAT);
 			ret->type = AT_FLOAT;
-			ret->flt = (float)c;
+			ret->flt = fc;
 		}
 	}
 	return 0;
diff --git a/test/box/update.result b/test/box/update.result
index a3f731b55df43f5acc3ad77f1265dfd3d4b7f891..0dbe0f0a1e932f7e8472971802555acf9d69afdd 100644
--- a/test/box/update.result
+++ b/test/box/update.result
@@ -849,3 +849,68 @@ s:update(1, {{'=', 3, map}})
 s:drop()
 ---
 ...
+--
+-- gh-4701: arith operations should not truncate result when
+-- float + float fits double.
+--
+msgpackffi = require('msgpackffi')
+---
+...
+mp_array_1 = 0x91
+---
+...
+mp_double = 0xcb
+---
+...
+mp_float = 0xca
+---
+...
+flt_max = 3.402823466e+38
+---
+...
+uint_max = 18446744073709551615ULL
+---
+...
+-- Double + double is float if result fits.                                     \
+-- Double + double is double if result does not fit float.                      \
+-- Double + float is float if result fits.                                      \
+-- Double + float is double if result does not fit float.                       \
+-- Float + float is float when no overflow.                                     \
+-- Float + float is double when overflow.                                       \
+-- Float + int is float when fits the float range.                              \
+-- Precision matters too. Double is used when need to avoid                     \
+-- precision loss.                                                              \
+tests = {                                                                       \
+    {{'double', 1}, {'double', 1}, mp_float},                                   \
+    {{'double', flt_max}, {'double', flt_max}, mp_double},                      \
+    {{'double', 1}, {'float', 1}, mp_float},                                    \
+    {{'double', flt_max}, {'float', flt_max}, mp_double},                       \
+    {{'float', 1}, {'float', 1}, mp_float},                                     \
+    {{'float', flt_max}, {'float', flt_max}, mp_double},                        \
+    {{'float', -flt_max}, {'float', -flt_max}, mp_double},                      \
+    {{'float', 1}, {'int', 1}, mp_float},                                       \
+    {{'float', flt_max}, {'uint64_t', uint_max}, mp_double},                    \
+    {{'float', 1.0001}, {'double', 1.0000000000001}, mp_double},                \
+}
+---
+...
+err = nil
+---
+...
+for i, test in pairs(tests) do                                                  \
+    local val1 = ffi.cast(test[1][1], test[1][2])                               \
+    local val2 = ffi.cast(test[2][1], test[2][2])                               \
+    local t = box.tuple.new({val1})                                             \
+    t = t:update({{'+', 1, val2}})                                              \
+    local m = msgpackffi.encode(t)                                              \
+    if m:byte(1) ~= mp_array_1 or m:byte(2) ~= test[3] then                     \
+        err = {i, test, t, m:byte(1), m:byte(2)}                                \
+        break                                                                   \
+    end                                                                         \
+end
+---
+...
+err
+---
+- null
+...
diff --git a/test/box/update.test.lua b/test/box/update.test.lua
index c455bc1e1e112a02892c6e2c397a7adec3a519ff..724011c38fe6acc2d86cfb00adbd2d6c35893889 100644
--- a/test/box/update.test.lua
+++ b/test/box/update.test.lua
@@ -269,3 +269,48 @@ t:update({{'=', 3, map}})
 s:update(1, {{'=', 3, map}})
 
 s:drop()
+
+--
+-- gh-4701: arith operations should not truncate result when
+-- float + float fits double.
+--
+msgpackffi = require('msgpackffi')
+mp_array_1 = 0x91
+mp_double = 0xcb
+mp_float = 0xca
+flt_max = 3.402823466e+38
+uint_max = 18446744073709551615ULL
+tests = {                                                                       \
+-- Double + double is float if result fits.                                     \
+    {{'double', 1}, {'double', 1}, mp_float},                                   \
+-- Double + double is double if result does not fit float.                      \
+    {{'double', flt_max}, {'double', flt_max}, mp_double},                      \
+-- Double + float is float if result fits.                                      \
+    {{'double', 1}, {'float', 1}, mp_float},                                    \
+-- Double + float is double if result does not fit float.                       \
+    {{'double', flt_max}, {'float', flt_max}, mp_double},                       \
+-- Float + float is float when no overflow.                                     \
+    {{'float', 1}, {'float', 1}, mp_float},                                     \
+-- Float + float is double when overflow.                                       \
+    {{'float', flt_max}, {'float', flt_max}, mp_double},                        \
+    {{'float', -flt_max}, {'float', -flt_max}, mp_double},                      \
+-- Float + int is float when fits the float range.                              \
+    {{'float', 1}, {'int', 1}, mp_float},                                       \
+    {{'float', flt_max}, {'uint64_t', uint_max}, mp_double},                    \
+-- Precision matters too. Double is used when need to avoid                     \
+-- precision loss.                                                              \
+    {{'float', 1.0001}, {'double', 1.0000000000001}, mp_double},                \
+}
+err = nil
+for i, test in pairs(tests) do                                                  \
+    local val1 = ffi.cast(test[1][1], test[1][2])                               \
+    local val2 = ffi.cast(test[2][1], test[2][2])                               \
+    local t = box.tuple.new({val1})                                             \
+    t = t:update({{'+', 1, val2}})                                              \
+    local m = msgpackffi.encode(t)                                              \
+    if m:byte(1) ~= mp_array_1 or m:byte(2) ~= test[3] then                     \
+        err = {i, test, t, m:byte(1), m:byte(2)}                                \
+        break                                                                   \
+    end                                                                         \
+end
+err