diff --git a/src/box/alter.cc b/src/box/alter.cc
index 92f1d5b221caa1da074746889535c71226fd2d61..4f2e34bf0d5a3003b5299bdc159e0237cd676d39 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -231,6 +231,23 @@ index_opts_decode(struct index_opts *opts, const char *map,
 	}
 }
 
+/**
+ * Helper routine for functional index function verification:
+ * only a deterministic persistent Lua function may be used in
+ * functional index for now.
+ */
+static void
+func_index_check_func(struct func *func) {
+	assert(func != NULL);
+	if (func->def->language != FUNC_LANGUAGE_LUA ||
+	    func->def->body == NULL || !func->def->is_deterministic ||
+	    !func->def->is_sandboxed) {
+		tnt_raise(ClientError, ER_WRONG_INDEX_OPTIONS, 0,
+			  "referenced function doesn't satisfy "
+			  "functional index function constraints");
+	}
+}
+
 /**
  * Create a index_def object from a record in _index
  * system space.
@@ -285,7 +302,8 @@ index_def_new_from_tuple(struct tuple *tuple, struct space *space)
 				 space->def->fields,
 				 space->def->field_count, &fiber()->gc) != 0)
 		diag_raise();
-	key_def = key_def_new(part_def, part_count, opts.func_id > 0);
+	bool for_func_index = opts.func_id > 0;
+	key_def = key_def_new(part_def, part_count, for_func_index);
 	if (key_def == NULL)
 		diag_raise();
 	struct index_def *index_def =
@@ -296,6 +314,26 @@ index_def_new_from_tuple(struct tuple *tuple, struct space *space)
 	auto index_def_guard = make_scoped_guard([=] { index_def_delete(index_def); });
 	index_def_check_xc(index_def, space_name(space));
 	space_check_index_def_xc(space, index_def);
+	/*
+	 * In case of functional index definition, resolve a
+	 * function pointer to perform a complete index build
+	 * (istead of initializing it in inactive state) in
+	 * on_replace_dd_index trigger. This allows wrap index
+	 * creation operation into transaction: only the first
+	 * opperation in transaction is allowed to yeld.
+	 *
+	 * The initialisation during recovery is slightly
+	 * different, because function cache is not initialized
+	 * during _index space loading. Therefore the completion
+	 * of a functional index creation is performed in
+	 * _func_index space's trigger, via IndexRebuild
+	 * operation.
+	 */
+	struct func *func = NULL;
+	if (for_func_index && (func = func_by_id(opts.func_id)) != NULL) {
+		func_index_check_func(func);
+		index_def_set_func(index_def, func);
+	}
 	if (index_def->iid == 0 && space->sequence != NULL)
 		index_def_check_sequence(index_def, space->sequence_fieldno,
 					 space->sequence_path,
@@ -4725,12 +4763,11 @@ on_replace_dd_func_index(struct trigger *trigger, void *event)
 		space = space_cache_find_xc(space_id);
 		index = index_find_xc(space, index_id);
 		func = func_cache_find(fid);
-		if (func->def->language != FUNC_LANGUAGE_LUA ||
-		    func->def->body == NULL || !func->def->is_deterministic ||
-		    !func->def->is_sandboxed) {
+		func_index_check_func(func);
+		if (index->def->opts.func_id != func->def->fid) {
 			tnt_raise(ClientError, ER_WRONG_INDEX_OPTIONS, 0,
-				  "referenced function doesn't satisfy "
-				  "functional index function constraints");
+				  "Function ids defined in _index and "
+				  "_func_index don't match");
 		}
 	} else if (old_tuple != NULL && new_tuple == NULL) {
 		uint32_t space_id = tuple_field_u32_xc(old_tuple,
@@ -4746,6 +4783,13 @@ on_replace_dd_func_index(struct trigger *trigger, void *event)
 			  "functional index", "alter");
 	}
 
+	/**
+	 * Index is already initialized for corresponding
+	 * function. Index rebuild is not required.
+	 */
+	if (index_def_get_func(index->def) == func)
+		return;
+
 	alter = alter_space_new(space);
 	auto scoped_guard = make_scoped_guard([=] {alter_space_delete(alter);});
 	alter_space_move_indexes(alter, 0, index->def->iid);
diff --git a/src/box/index_def.h b/src/box/index_def.h
index 62578bd607a7b2aed003d42afbbbaed1eeec4b4c..d928b23c7da45e812bb3cb5aab80028ccc397014 100644
--- a/src/box/index_def.h
+++ b/src/box/index_def.h
@@ -321,6 +321,18 @@ index_def_set_func(struct index_def *def, struct func *func)
 	def->cmp_def->func_index_func = NULL;
 }
 
+/**
+ * Get a func pointer by index definition.
+ * @param def Index def, containing key definitions.
+ * @returns not NULL function pointer when index definition
+ *          refers to function and NULL otherwise.
+ */
+static inline struct func *
+index_def_get_func(struct index_def *def)
+{
+	return def->key_def->func_index_func;
+}
+
 /**
  * Add an index definition to a list, preserving the
  * first position of the primary key.
diff --git a/test/engine/func_index.result b/test/engine/func_index.result
index 877b76d5ead679af769abf7c396e95af4aca5cf4..bb4200f7a6700dfea2d8115279dad9fd98f124aa 100644
--- a/test/engine/func_index.result
+++ b/test/engine/func_index.result
@@ -58,27 +58,18 @@ _ = s:create_index('idx', {func = box.func.s_nonpersistent.id, parts = {{1, 'uns
  | - error: 'Wrong index options (field 0): referenced function doesn''t satisfy functional
  |     index function constraints'
  | ...
-s.index.idx:drop()
- | ---
- | ...
 -- Can't use non-deterministic function in functional index.
 _ = s:create_index('idx', {func = box.func.s_ivaliddef1.id, parts = {{1, 'unsigned'}}})
  | ---
  | - error: 'Wrong index options (field 0): referenced function doesn''t satisfy functional
  |     index function constraints'
  | ...
-s.index.idx:drop()
- | ---
- | ...
 -- Can't use non-sandboxed function in functional index.
 _ = s:create_index('idx', {func = box.func.s_ivaliddef2.id, parts = {{1, 'unsigned'}}})
  | ---
  | - error: 'Wrong index options (field 0): referenced function doesn''t satisfy functional
  |     index function constraints'
  | ...
-s.index.idx:drop()
- | ---
- | ...
 -- Can't use non-sequential parts in returned key definition.
 _ = s:create_index('idx', {func = box.func.ss.id, parts = {{1, 'unsigned'}, {3, 'unsigned'}}})
  | ---
@@ -731,3 +722,90 @@ box.schema.func.drop('s')
 box.schema.func.drop('sub')
  | ---
  | ...
+
+--
+-- gh-4401: make functional index creation transactional
+--
+test_run:cmd("setopt delimiter ';'")
+ | ---
+ | - true
+ | ...
+function test1()
+   lua_code = [[function(tuple) return {tuple[1] + tuple[2]} end]]
+   box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+   box.schema.func.create('extr1', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+   s = box.schema.space.create('withdata')
+   pk = s:create_index('pk')
+   box.space._index:insert({s.id, 2, 'idx', 'tree', {unique=true, func=box.func.extr.id}, {{0, 'integer'}}})
+   box.space._func_index:insert({s.id, 2, box.func.extr1.id})
+end
+test_run:cmd("setopt delimiter ''");
+ | ---
+ | ...
+
+box.atomic(test1)
+ | ---
+ | - error: 'Wrong index options (field 0): Function ids defined in _index and _func_index
+ |     don''t match'
+ | ...
+
+box.func.extr1 == nil
+ | ---
+ | - true
+ | ...
+box.func.extr == nil
+ | ---
+ | - true
+ | ...
+box.is_in_txn()
+ | ---
+ | - false
+ | ...
+box.space._space.index.name:count('withdata') == 0
+ | ---
+ | - true
+ | ...
+
+-- Test successful index creation
+s = box.schema.space.create('withdata', {engine = engine})
+ | ---
+ | ...
+lua_code = [[function(tuple) return {tuple[1] + tuple[2]} end]]
+ | ---
+ | ...
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+ | ---
+ | ...
+pk = s:create_index('pk')
+ | ---
+ | ...
+test_run:cmd("setopt delimiter ';'")
+ | ---
+ | - true
+ | ...
+function test2()
+    idx = s:create_index('idx', {unique = true, func = 'extr', parts = {{1, 'integer'}}})
+end
+test_run:cmd("setopt delimiter ''");
+ | ---
+ | ...
+
+box.atomic(test2)
+ | ---
+ | ...
+
+s:insert({1, 2})
+ | ---
+ | - [1, 2]
+ | ...
+idx:get({3})
+ | ---
+ | - [1, 2]
+ | ...
+
+s:drop()
+ | ---
+ | ...
+box.func.extr:drop()
+ | ---
+ | ...
diff --git a/test/engine/func_index.test.lua b/test/engine/func_index.test.lua
index 372ec800d2547945568d1598f99b6b0e008877fc..f31162c97c333452c79185251ba1d333886868a7 100644
--- a/test/engine/func_index.test.lua
+++ b/test/engine/func_index.test.lua
@@ -22,13 +22,10 @@ _ = s:create_index('idx', {func = 6666, parts = {{1, 'unsigned'}}})
 s.index.idx:drop()
 -- Can't use non-persistent function in functional index.
 _ = s:create_index('idx', {func = box.func.s_nonpersistent.id, parts = {{1, 'unsigned'}}})
-s.index.idx:drop()
 -- Can't use non-deterministic function in functional index.
 _ = s:create_index('idx', {func = box.func.s_ivaliddef1.id, parts = {{1, 'unsigned'}}})
-s.index.idx:drop()
 -- Can't use non-sandboxed function in functional index.
 _ = s:create_index('idx', {func = box.func.s_ivaliddef2.id, parts = {{1, 'unsigned'}}})
-s.index.idx:drop()
 -- Can't use non-sequential parts in returned key definition.
 _ = s:create_index('idx', {func = box.func.ss.id, parts = {{1, 'unsigned'}, {3, 'unsigned'}}})
 -- Can't use parts started not by 1 field.
@@ -248,3 +245,44 @@ idx2:get(3)
 s:drop()
 box.schema.func.drop('s')
 box.schema.func.drop('sub')
+
+--
+-- gh-4401: make functional index creation transactional
+--
+test_run:cmd("setopt delimiter ';'")
+function test1()
+   lua_code = [[function(tuple) return {tuple[1] + tuple[2]} end]]
+   box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+   box.schema.func.create('extr1', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+   s = box.schema.space.create('withdata')
+   pk = s:create_index('pk')
+   box.space._index:insert({s.id, 2, 'idx', 'tree', {unique=true, func=box.func.extr.id}, {{0, 'integer'}}})
+   box.space._func_index:insert({s.id, 2, box.func.extr1.id})
+end
+test_run:cmd("setopt delimiter ''");
+
+box.atomic(test1)
+
+box.func.extr1 == nil
+box.func.extr == nil
+box.is_in_txn()
+box.space._space.index.name:count('withdata') == 0
+
+-- Test successful index creation
+s = box.schema.space.create('withdata', {engine = engine})
+lua_code = [[function(tuple) return {tuple[1] + tuple[2]} end]]
+box.schema.func.create('extr', {body = lua_code, is_deterministic = true, is_sandboxed = true})
+pk = s:create_index('pk')
+test_run:cmd("setopt delimiter ';'")
+function test2()
+    idx = s:create_index('idx', {unique = true, func = 'extr', parts = {{1, 'integer'}}})
+end
+test_run:cmd("setopt delimiter ''");
+
+box.atomic(test2)
+
+s:insert({1, 2})
+idx:get({3})
+
+s:drop()
+box.func.extr:drop()