diff --git a/core/modules/filter/src/Plugin/migrate/process/FilterID.php b/core/modules/filter/src/Plugin/migrate/process/FilterID.php
index 14f88408a1c462abc67723e9c38cdea92caca8b6..27973eae16050cf7d0a60e92cf9915e8f90ed0ae 100644
--- a/core/modules/filter/src/Plugin/migrate/process/FilterID.php
+++ b/core/modules/filter/src/Plugin/migrate/process/FilterID.php
@@ -7,7 +7,6 @@
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\filter\Plugin\FilterInterface;
 use Drupal\migrate\MigrateExecutableInterface;
-use Drupal\migrate\MigrateSkipProcessException;
 use Drupal\migrate\Plugin\migrate\process\StaticMap;
 use Drupal\migrate\Plugin\MigrationInterface;
 use Drupal\migrate\Row;
@@ -86,7 +85,8 @@ public function transform($value, MigrateExecutableInterface $migrate_executable
       if (in_array(static::getSourceFilterType($value), [FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE], TRUE)) {
         $message = sprintf('Filter %s could not be mapped to an existing filter plugin; omitted since it is a transformation-only filter. Install and configure a successor after the migration.', $plugin_id);
         $migrate_executable->saveMessage($message, MigrationInterface::MESSAGE_INFORMATIONAL);
-        throw new MigrateSkipProcessException("The transformation-only filter $plugin_id was skipped.");
+        $this->stopPipeline();
+        return NULL;
       }
       $fallback = $this->filterManager->getFallbackPluginId($plugin_id);
 
diff --git a/core/modules/filter/tests/src/Kernel/Plugin/migrate/process/FilterIdTest.php b/core/modules/filter/tests/src/Kernel/Plugin/migrate/process/FilterIdTest.php
index 04dfe6818fa931ad1fd04724ec58e38a96a5d698..9435f71c438b7d91c03698e40b52498228158477 100644
--- a/core/modules/filter/tests/src/Kernel/Plugin/migrate/process/FilterIdTest.php
+++ b/core/modules/filter/tests/src/Kernel/Plugin/migrate/process/FilterIdTest.php
@@ -5,7 +5,6 @@
 use Drupal\filter\Plugin\migrate\process\FilterID;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\migrate\MigrateExecutableInterface;
-use Drupal\migrate\MigrateSkipProcessException;
 use Drupal\migrate\Plugin\MigrationInterface;
 use Drupal\migrate\Row;
 
@@ -47,7 +46,7 @@ protected function setUp(): void {
    * @param string $invalid_id
    *   (optional) The invalid plugin ID which is expected to be logged by the
    *   MigrateExecutable object.
-   * @param bool $skip_exception
+   * @param bool $stop_pipeline
    *   (optional) Set to TRUE if we expect the filter to be skipped because it
    *   is a transformation-only filter.
    *
@@ -55,7 +54,7 @@ protected function setUp(): void {
    *
    * @covers ::transform
    */
-  public function testTransform($value, $expected_value, $invalid_id = NULL, $skip_exception = FALSE) {
+  public function testTransform($value, $expected_value, $invalid_id = NULL, $stop_pipeline = FALSE) {
     $configuration = [
       'bypass' => TRUE,
       'map' => [
@@ -65,7 +64,7 @@ public function testTransform($value, $expected_value, $invalid_id = NULL, $skip
     ];
     $plugin = FilterID::create($this->container, $configuration, 'filter_id', []);
 
-    if ($skip_exception) {
+    if ($stop_pipeline) {
       $this->executable
         ->expects($this->exactly(1))
         ->method('saveMessage')
@@ -73,8 +72,6 @@ public function testTransform($value, $expected_value, $invalid_id = NULL, $skip
           sprintf('Filter %s could not be mapped to an existing filter plugin; omitted since it is a transformation-only filter. Install and configure a successor after the migration.', $value),
           MigrationInterface::MESSAGE_INFORMATIONAL
         );
-      $this->expectException(MigrateSkipProcessException::class);
-      $this->expectExceptionMessage(sprintf("The transformation-only filter %s was skipped.", $value));
     }
 
     if (isset($invalid_id)) {
@@ -91,6 +88,7 @@ public function testTransform($value, $expected_value, $invalid_id = NULL, $skip
     $output_value = $plugin->transform($value, $this->executable, $row, 'foo');
 
     $this->assertSame($expected_value, $output_value);
+    $this->assertSame($stop_pipeline, $plugin->isPipelineStopped());
   }
 
   /**
@@ -128,7 +126,7 @@ public static function provideFilters() {
       ],
       'transformation-only D7 contrib filter' => [
         'editor_align',
-        '',
+        NULL,
         NULL,
         TRUE,
       ],
diff --git a/core/modules/migrate/src/Plugin/migrate/process/MigrationLookup.php b/core/modules/migrate/src/Plugin/migrate/process/MigrationLookup.php
index 8e7ca733a9c2f2e5879ae6ec6fe916a8032857c8..367ed28153fde9d0df1fdd838ccc89834ed05717 100644
--- a/core/modules/migrate/src/Plugin/migrate/process/MigrationLookup.php
+++ b/core/modules/migrate/src/Plugin/migrate/process/MigrationLookup.php
@@ -6,7 +6,6 @@
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\migrate\MigrateException;
 use Drupal\migrate\MigrateLookupInterface;
-use Drupal\migrate\MigrateSkipProcessException;
 use Drupal\migrate\MigrateSkipRowException;
 use Drupal\migrate\MigrateStubInterface;
 use Drupal\migrate\ProcessPluginBase;
@@ -114,9 +113,11 @@
  * @endcode
  *
  * If the source value passed in to the plugin is NULL, boolean FALSE, an empty
- * array or an empty string, the plugin will throw a
- * MigrateSkipProcessException, causing further plugins in the process to be
- * skipped.
+ * array or an empty string, the plugin will return NULL and stop further
+ * processing on the pipeline. This is done for backwards compatibility reasons,
+ * and future versions of this plugin should simply return NULL and allow
+ * processing to continue.
+ * @see https://www.drupal.org/project/drupal/issues/3246666
  *
  * @see \Drupal\migrate\Plugin\MigrateProcessInterface
  *
@@ -187,7 +188,6 @@ public static function create(ContainerInterface $container, array $configuratio
   /**
    * {@inheritdoc}
    *
-   * @throws \Drupal\migrate\MigrateSkipProcessException
    * @throws \Drupal\migrate\MigrateException
    */
   public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
@@ -205,6 +205,9 @@ public function transform($value, MigrateExecutableInterface $migrate_executable
       }
       $lookup_value = (array) $lookup_value;
       $this->skipInvalid($lookup_value);
+      if ($this->isPipelineStopped()) {
+        return NULL;
+      }
       $source_id_values[$lookup_migration_id] = $lookup_value;
 
       // Re-throw any PluginException as a MigrateException so the executable
@@ -281,12 +284,10 @@ public function transform($value, MigrateExecutableInterface $migrate_executable
    *
    * @param array $value
    *   The incoming value to check.
-   *
-   * @throws \Drupal\migrate\MigrateSkipProcessException
    */
   protected function skipInvalid(array $value) {
     if (!array_filter($value, [$this, 'isValid'])) {
-      throw new MigrateSkipProcessException();
+      $this->stopPipeline();
     }
   }
 
diff --git a/core/modules/migrate/src/Plugin/migrate/process/SkipOnEmpty.php b/core/modules/migrate/src/Plugin/migrate/process/SkipOnEmpty.php
index 4d0a7625c8af67edb5169a9bc215854459e4fd2b..cc258bc6da0d55f4ef8b67040a49ddca41a08a77 100644
--- a/core/modules/migrate/src/Plugin/migrate/process/SkipOnEmpty.php
+++ b/core/modules/migrate/src/Plugin/migrate/process/SkipOnEmpty.php
@@ -123,10 +123,6 @@ public function row($value, MigrateExecutableInterface $migrate_executable, Row
    *
    * @return mixed
    *   The input value, $value, if it is not empty.
-   *
-   * @throws \Drupal\migrate\MigrateSkipProcessException
-   *   Thrown if the source property is not set and rest of the process should
-   *   be skipped.
    */
   public function process($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
     if (!$value) {
diff --git a/core/modules/migrate/src/Plugin/migrate/process/SubProcess.php b/core/modules/migrate/src/Plugin/migrate/process/SubProcess.php
index 29115b66e306bf1aa5842e82cf38b9a725cb3354..1f7d6b8a94ffe382dd8a9043d7c0d316ed410a03 100644
--- a/core/modules/migrate/src/Plugin/migrate/process/SubProcess.php
+++ b/core/modules/migrate/src/Plugin/migrate/process/SubProcess.php
@@ -220,9 +220,8 @@ public function transform($value, MigrateExecutableInterface $migrate_executable
           $key = $this->transformKey($key, $migrate_executable, $new_row);
         }
         // Do not save the result if the key is NULL. The configured process
-        // pipeline used in transformKey() will return NULL if a
-        // MigrateSkipProcessException is thrown.
-        // @see \Drupal\filter\Plugin\migrate\process\FilterID
+        // pipeline used in transformKey() will return NULL if the key can not
+        // be transformed.
         if ($key !== NULL) {
           $return[$key] = $destination;
         }
diff --git a/core/modules/migrate/tests/src/Unit/process/MigrationLookupTest.php b/core/modules/migrate/tests/src/Unit/process/MigrationLookupTest.php
index 5699e30723d69783b0f00ab214b5f0b58a9aa83e..d054deb50d08fddac73aa2831f9e47d4be6b1e29 100644
--- a/core/modules/migrate/tests/src/Unit/process/MigrationLookupTest.php
+++ b/core/modules/migrate/tests/src/Unit/process/MigrationLookupTest.php
@@ -5,7 +5,6 @@
 namespace Drupal\Tests\migrate\Unit\process;
 
 use Drupal\migrate\MigrateException;
-use Drupal\migrate\MigrateSkipProcessException;
 use Drupal\migrate\Plugin\MigrationInterface;
 use Drupal\migrate\Plugin\migrate\process\MigrationLookup;
 use Drupal\migrate\Plugin\MigrateIdMapInterface;
@@ -90,8 +89,9 @@ public function testSkipInvalid($value) {
     $migration_plugin_manager->createInstances(['foo'])
       ->willReturn(['foo' => $migration_plugin->reveal()]);
     $migration = MigrationLookup::create($this->prepareContainer(), $configuration, '', [], $migration_plugin->reveal());
-    $this->expectException(MigrateSkipProcessException::class);
-    $migration->transform($value, $this->migrateExecutable, $this->row, 'foo');
+    $result = $migration->transform($value, $this->migrateExecutable, $this->row, 'foo');
+    $this->assertTrue($migration->isPipelineStopped());
+    $this->assertNull($result);
   }
 
   /**
@@ -164,9 +164,10 @@ public static function noSkipValidDataProvider() {
    * @param string|array $expected_value
    *   The expected value(s) of the migration process plugin.
    *
-   * @dataProvider successfulLookupDataProvider
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   * @throws \Drupal\migrate\MigrateException
    *
-   * @throws \Drupal\migrate\MigrateSkipProcessException
+   * @dataProvider successfulLookupDataProvider
    */
   public function testSuccessfulLookup(array $source_id_values, array $destination_id_values, $source_value, $expected_value) {
     $migration_plugin = $this->prophesize(MigrationInterface::class);