diff --git a/core/core.services.yml b/core/core.services.yml
index 98c87ca..ed1341c 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -338,7 +338,7 @@ services:
     arguments: ['@request_stack', '@url_generator']
   form_error_handler:
     class: Drupal\Core\Form\FormErrorHandler
-    arguments: ['@string_translation', '@link_generator']
+    arguments: ['@string_translation', '@link_generator', '@renderer']
   form_cache:
     class: Drupal\Core\Form\FormCache
     arguments: ['@app.root', '@keyvalue.expirable', '@module_handler', '@current_user', '@csrf_token', '@logger.channel.form', '@request_stack', '@page_cache_request_policy']
diff --git a/core/lib/Drupal/Core/Form/FormErrorHandler.php b/core/lib/Drupal/Core/Form/FormErrorHandler.php
index df736b0..870fbf0 100644
--- a/core/lib/Drupal/Core/Form/FormErrorHandler.php
+++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php
@@ -7,9 +7,9 @@
 
 namespace Drupal\Core\Form;
 
-use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Routing\LinkGeneratorTrait;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\Url;
@@ -24,16 +24,26 @@ class FormErrorHandler implements FormErrorHandlerInterface {
   use LinkGeneratorTrait;
 
   /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
    * Constructs a new FormErrorHandler.
    *
    * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
    *   The string translation service.
    * @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator
    *   The link generation service.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
    */
-  public function __construct(TranslationInterface $string_translation, LinkGeneratorInterface $link_generator) {
+  public function __construct(TranslationInterface $string_translation, LinkGeneratorInterface $link_generator, RendererInterface $renderer) {
     $this->stringTranslation = $string_translation;
     $this->linkGenerator = $link_generator;
+    $this->renderer = $renderer;
   }
 
   /**
@@ -82,9 +92,7 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st
         unset($errors[$name]);
       }
       elseif ($is_visible_element && $has_title && $has_id) {
-        // We need to pass this through SafeMarkup::escape() so
-        // drupal_set_message() does not encode the links.
-        $error_links[] = SafeMarkup::escape($this->l($title, Url::fromRoute('<none>', [], ['fragment' => $form_element['#id'], 'external' => TRUE])));
+        $error_links[] = $this->l($title, Url::fromRoute('<none>', [], ['fragment' => $form_element['#id'], 'external' => TRUE]));
         unset($errors[$name]);
       }
     }
@@ -95,9 +103,17 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st
     }
 
     if (!empty($error_links)) {
-      $message = $this->formatPlural(count($error_links), '1 error has been found: !errors', '@count errors have been found: !errors', [
-        '!errors' => SafeMarkup::set(implode(', ', $error_links)),
-      ]);
+      $render_array = [
+        [
+         '#markup' => $this->formatPlural(count($error_links), '1 error has been found: ', '@count errors have been found: '),
+        ],
+        [
+          '#theme' => 'item_list',
+          '#items' => $error_links,
+          '#context' => ['list_style' => 'comma-list'],
+        ],
+      ];
+      $message = $this->renderer->renderPlain($render_array);
       $this->drupalSetMessage($message, 'error');
     }
   }
diff --git a/core/modules/datetime/src/Tests/DateTimeFieldTest.php b/core/modules/datetime/src/Tests/DateTimeFieldTest.php
index d5d14a4..57ff987 100644
--- a/core/modules/datetime/src/Tests/DateTimeFieldTest.php
+++ b/core/modules/datetime/src/Tests/DateTimeFieldTest.php
@@ -589,11 +589,11 @@ function testDatelistWidget() {
   protected function datelistDataProvider() {
     return [
       // Year only selected, validation error on Month, Day, Hour, Minute.
-      [['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''], '4 errors have been found: Month, Day, Hour, Minute'],
+      [['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''], '4 errors have been found: Month' . 'Day' . 'Hour' . 'Minute'],
       // Year and Month selected, validation error on Day, Hour, Minute.
-      [['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''], '3 errors have been found: Day, Hour, Minute'],
+      [['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''], '3 errors have been found: Day' . 'Hour' . 'Minute'],
       // Year, Month and Day selected, validation error on Hour, Minute.
-      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''], '2 errors have been found: Hour, Minute'],
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''], '2 errors have been found: Hour' . 'Minute'],
       // Year, Month, Day and Hour selected, validation error on Minute only.
       [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''], '1 error has been found: Minute'],
     ];
diff --git a/core/modules/shortcut/src/Tests/ShortcutSetsTest.php b/core/modules/shortcut/src/Tests/ShortcutSetsTest.php
index 9334ea1..26a3171 100644
--- a/core/modules/shortcut/src/Tests/ShortcutSetsTest.php
+++ b/core/modules/shortcut/src/Tests/ShortcutSetsTest.php
@@ -132,7 +132,8 @@ function testShortcutSetSwitchCreate() {
   function testShortcutSetSwitchNoSetName() {
     $edit = array('set' => 'new');
     $this->drupalPostForm('user/' . $this->adminUser->id() . '/shortcuts', $edit, t('Change set'));
-    $this->assertRaw('1 error has been found: <a href="#edit-label">Label</a>');
+    $this->assertRaw('1 error has been found:');
+    $this->assertRaw('<a href="#edit-label">Label</a>');
     $current_set = shortcut_current_displayed_set($this->adminUser);
     $this->assertEqual($current_set->id(), $this->set->id(), 'Attempting to switch to a new shortcut set without providing a set name does not succeed.');
     $this->assertFieldByXPath("//input[@name='label' and contains(concat(' ', normalize-space(@class), ' '), ' error ')]", NULL, 'The new set label field has the error class');
diff --git a/core/modules/system/css/components/item-list.theme.css b/core/modules/system/css/components/item-list.theme.css
index 9a1088e..185e450 100644
--- a/core/modules/system/css/components/item-list.theme.css
+++ b/core/modules/system/css/components/item-list.theme.css
@@ -2,21 +2,6 @@
  * @file
  * Visual styles for item list.
  */
-
-.item-list .title {
-  font-weight: bold;
-}
-.item-list ul {
-  margin: 0 0 0.75em 0;
-  padding: 0;
-}
-.item-list ul li {
-  margin: 0 0 0.25em 1.5em; /* LTR */
-  padding: 0;
-}
-[dir="rtl"] .item-list ul li {
-  margin: 0 1.5em 0.25em 0;
-}
 ul.item-list__comma-list {
   display: inline;
 }
diff --git a/core/modules/system/src/Tests/Form/ValidationTest.php b/core/modules/system/src/Tests/Form/ValidationTest.php
index 246fb68..1d130f9 100644
--- a/core/modules/system/src/Tests/Form/ValidationTest.php
+++ b/core/modules/system/src/Tests/Form/ValidationTest.php
@@ -292,7 +292,10 @@ protected function assertErrorMessages($messages) {
       $error_links[] = \Drupal::l($message['title'], Url::fromRoute('<none>', [], ['fragment' => 'edit-' . str_replace('_', '-', $message['key']), 'external' => TRUE]));
     }
     $top_message = \Drupal::translation()->formatPlural(count($error_links), '1 error has been found:', '@count errors have been found:');
-    $this->assertRaw($top_message . ' ' . implode(', ', $error_links));
+    $this->assertRaw($top_message);
+    foreach ($error_links as $error_link) {
+      $this->assertRaw($error_link);
+    }
     $this->assertNoText('An illegal choice has been detected. Please contact the site administrator.');
   }
 
diff --git a/core/modules/user/src/Tests/UserBlocksTest.php b/core/modules/user/src/Tests/UserBlocksTest.php
index e92640b..9ac5ae9 100644
--- a/core/modules/user/src/Tests/UserBlocksTest.php
+++ b/core/modules/user/src/Tests/UserBlocksTest.php
@@ -48,7 +48,8 @@ function testUserLoginBlock() {
     $edit['name'] = $this->randomMachineName();
     $edit['pass'] = $this->randomMachineName();
     $this->drupalPostForm('node', $edit, t('Log in'));
-    $this->assertRaw('1 error has been found: <a href="#edit-name">Username</a>');
+    $this->assertRaw('1 error has been found:');
+    $this->assertRaw('<a href="#edit-name">Username</a>');
     $this->assertText(t('Sorry, unrecognized username or password.'));
 
     // Create a user with some permission that anonymous users lack.
diff --git a/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php b/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php
index 53d7e76..cd8e3b2 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php
@@ -25,8 +25,9 @@ public function testDisplayErrorMessages() {
     $link_generator->expects($this->any())
       ->method('generate')
       ->willReturnArgument(0);
+    $renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
     $form_error_handler = $this->getMockBuilder('Drupal\Core\Form\FormErrorHandler')
-      ->setConstructorArgs([$this->getStringTranslationStub(), $link_generator])
+      ->setConstructorArgs([$this->getStringTranslationStub(), $link_generator, $renderer])
       ->setMethods(['drupalSetMessage'])
       ->getMock();
 
@@ -41,7 +42,13 @@ public function testDisplayErrorMessages() {
       ->with('this missing element is invalid', 'error');
     $form_error_handler->expects($this->at(3))
       ->method('drupalSetMessage')
-      ->with('3 errors have been found: Test 1, Test 2 &amp; a half, Test 3', 'error');
+      ->with('3 errors have been found: <ul-comma-list-mock><li-mock>Test 1</li-mock><li-mock>Test 2 &amp; a half</li-mock><li-mock>Test 3</li-mock></ul-comma-list-mock>', 'error');
+
+    $renderer->expects($this->any())
+      ->method('renderPlain')
+      ->will($this->returnCallback(function ($render_array) {
+        return $render_array[0]['#markup'] . '<ul-comma-list-mock><li-mock>' . implode(array_map('htmlspecialchars', $render_array[1]['#items']), '</li-mock><li-mock>') . '</li-mock></ul-comma-list-mock>';
+      }));
 
     $form = [
       '#parents' => [],
@@ -103,7 +110,7 @@ public function testDisplayErrorMessages() {
    */
   public function testSetElementErrorsFromFormState() {
     $form_error_handler = $this->getMockBuilder('Drupal\Core\Form\FormErrorHandler')
-      ->setConstructorArgs([$this->getStringTranslationStub(), $this->getMock('Drupal\Core\Utility\LinkGeneratorInterface')])
+      ->setConstructorArgs([$this->getStringTranslationStub(), $this->getMock('Drupal\Core\Utility\LinkGeneratorInterface'), $this->getMock('\Drupal\Core\Render\RendererInterface')])
       ->setMethods(['drupalSetMessage'])
       ->getMock();
 
diff --git a/core/themes/bartik/css/components/item-list.css b/core/themes/bartik/css/components/item-list.css
index 698bd56..eeb4e77 100644
--- a/core/themes/bartik/css/components/item-list.css
+++ b/core/themes/bartik/css/components/item-list.css
@@ -10,10 +10,3 @@
 [dir="rtl"] .item-list ul li {
   padding: 0.2em 0 0 0.5em;
 }
-.item-list .item-list__comma-list,
-.item-list .item-list__comma-list li,
-[dir="rtl"] .item-list .item-list__comma-list,
-[dir="rtl"] .item-list .item-list__comma-list li {
-  margin: 0;
-  padding: 0;
-}
diff --git a/core/themes/classy/classy.libraries.yml b/core/themes/classy/classy.libraries.yml
index a5bde77..2a0831a 100644
--- a/core/themes/classy/classy.libraries.yml
+++ b/core/themes/classy/classy.libraries.yml
@@ -3,6 +3,9 @@ base:
   css:
     theme:
       css/layout.css: {}
+      # We can not attach this in the item-list.html.twig template since it is
+      # often used during RendererInterface::renderPlain().
+      css/dataset/item-list.css: {}
 
 book-navigation:
   version: VERSION
diff --git a/core/themes/classy/css/dataset/item-list.css b/core/themes/classy/css/dataset/item-list.css
new file mode 100644
index 0000000..bce0456
--- /dev/null
+++ b/core/themes/classy/css/dataset/item-list.css
@@ -0,0 +1,38 @@
+/**
+ * @file
+ * Styling for item lists.
+ */
+.item-list .title {
+  font-weight: bold;
+}
+.item-list ul {
+  margin: 0 0 0.75em 0;
+  padding: 0;
+}
+.item-list ul li {
+  margin: 0 0 0.25em 1.5em; /* LTR */
+  padding: 0;
+}
+[dir="rtl"] .item-list ul li {
+  margin: 0 1.5em 0.25em 0;
+}
+div.item-list__comma-list {
+  display: inline;
+}
+/**
+ * Comma separated lists inside lists.
+ */
+.item-list ul.item-list__comma-list {
+  display: inline;
+}
+.item-list ul.item-list__comma-list li {
+  display: inline;
+  list-style-type: none;
+}
+.item-list .item-list__comma-list,
+.item-list .item-list__comma-list li,
+[dir="rtl"] .item-list .item-list__comma-list,
+[dir="rtl"] .item-list .item-list__comma-list li {
+  margin: 0;
+  padding: 0;
+}
diff --git a/core/themes/classy/templates/dataset/item-list.html.twig b/core/themes/classy/templates/dataset/item-list.html.twig
index 7ba8be4..abecd47 100644
--- a/core/themes/classy/templates/dataset/item-list.html.twig
+++ b/core/themes/classy/templates/dataset/item-list.html.twig
@@ -18,11 +18,14 @@
  * @see template_preprocess_item_list()
  */
 #}
-{% if context.list_style %}
-  {% set attributes = attributes.addClass('item-list__' ~ context.list_style) %}
-{% endif %}
+{%- if context.list_style -%}
+  {%- set attributes = attributes.addClass('item-list__' ~ context.list_style) -%}
+  {%- set wrapper_class = 'item-list item-list__' ~ context.list_style -%}
+{%- else -%}
+  {%- set wrapper_class = 'item-list' -%}
+{%- endif -%}
 {%- if items or empty -%}
-  <div class="item-list">
+  <div class="{{ wrapper_class }}">
     {%- if title is not empty -%}
       <h3>{{ title }}</h3>
     {%- endif -%}
