diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index add0d65f816..4bca240f367 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -464,7 +464,7 @@ tuple_lock_retry:
 						HeapTupleHeaderGetCmin(tuple->t_data) >= cid)
 					{
 						ReleaseBuffer(buffer);
-						return TM_Invisible;
+						return TM_SelfModified;
 					}
 
 					/*
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 61c4459f676..e270dd26a37 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -858,6 +858,27 @@ ldelete:;
 							else
 								goto ldelete;
 
+						case TM_SelfModified:
+							/*
+							 * This can be reached when following an update
+							 * chain from a tuple updated by this session in
+							 * an earlier command, reaching a tuple that was
+							 * already updated in this command.
+							 *
+							 * See also TM_SelfModified response to
+							 * table_delete() above.
+							 */
+							/*
+							 * XXX: says "to be updated" - as above - but only
+							 * because it historically said so.
+							 */
+							if (tmfd.cmax != estate->es_output_cid)
+								ereport(ERROR,
+										(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
+										 errmsg("tuple to be updated was already modified by an operation triggered by the current command"),
+										 errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));
+							return NULL;
+
 						case TM_Deleted:
 							/* tuple already deleted; nothing to do */
 							return NULL;
@@ -870,10 +891,6 @@ ldelete:;
 							 * already have errored out if the first version
 							 * is invisible.
 							 *
-							 * TM_SelfModified should be impossible, as we'd
-							 * otherwise should have hit the TM_SelfModified
-							 * case in response to table_delete above.
-							 *
 							 * TM_Updated should be impossible, because we're
 							 * locking the latest version via
 							 * TUPLE_LOCK_FLAG_FIND_LAST_VERSION.
@@ -1379,6 +1396,23 @@ lreplace:;
 							/* tuple already deleted; nothing to do */
 							return NULL;
 
+						case TM_SelfModified:
+							/*
+							 * This can be reached when following an update
+							 * chain from a tuple updated by this session in
+							 * an earlier command, reaching a tuple that was
+							 * already deleted in this command.
+							 *
+							 * See also TM_SelfModified response to
+							 * table_update() above.
+							 */
+							if (tmfd.cmax != estate->es_output_cid)
+								ereport(ERROR,
+										(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
+										 errmsg("tuple to be updated was already modified by an operation triggered by the current command"),
+										 errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));
+							return NULL;
+
 						default:
 							/* see table_lock_tuple call in ExecDelete() */
 							elog(ERROR, "unexpected table_lock_tuple status: %u",
