diff --git a/core/lib/Drupal/Component/Utility/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php
index d20ad6b..6d7042e 100644
--- a/core/lib/Drupal/Component/Utility/SafeMarkup.php
+++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php
@@ -215,37 +215,42 @@ public static function checkPlain($text) {
    * @see t()
    */
   public static function format($string, array $args) {
-    $safe = TRUE;
+    $all_safe = TRUE;
 
     // Transform arguments before inserting them.
     foreach ($args as $key => $value) {
+      $safe = static::isSafe($value);
+      if ($value instanceof SafeStringInterface) {
+        $value = $value->toString();
+      }
       switch ($key[0]) {
         case '@':
           // Escaped only.
-          if (!SafeMarkup::isSafe($value)) {
-            $args[$key] = Html::escape($value);
+          if (!$safe) {
+            $value = Html::escape($value);
           }
           break;
 
         case '%':
         default:
           // Escaped and placeholder.
-          if (!SafeMarkup::isSafe($value)) {
+          if (!$safe) {
             $value = Html::escape($value);
           }
-          $args[$key] = '<em class="placeholder">' . $value . '</em>';
+          $value = '<em class="placeholder">' . $value . '</em>';
           break;
 
         case '!':
           // Pass-through.
-          if (!static::isSafe($value)) {
-            $safe = FALSE;
+          if (!$safe) {
+            $all_safe = FALSE;
           }
       }
+      $args[$key] = $value;
     }
 
     $output = strtr($string, $args);
-    if ($safe) {
+    if ($all_safe) {
       static::$safeStrings[$output]['html'] = TRUE;
     }
 
diff --git a/core/lib/Drupal/Component/Utility/SafeStringInterface.php b/core/lib/Drupal/Component/Utility/SafeStringInterface.php
index 45d536d..bc15b73 100644
--- a/core/lib/Drupal/Component/Utility/SafeStringInterface.php
+++ b/core/lib/Drupal/Component/Utility/SafeStringInterface.php
@@ -33,7 +33,7 @@
  * @see \Drupal\Component\Utility\SafeMarkup::isSafe()
  * @see \Drupal\Core\Template\TwigExtension::escapeFilter()
  */
-interface SafeStringInterface extends \JsonSerializable {
+interface SafeStringInterface extends ToStringInterface, \JsonSerializable {
 
   /**
    * Returns a safe string.
diff --git a/core/lib/Drupal/Component/Utility/SafeStringTrait.php b/core/lib/Drupal/Component/Utility/SafeStringTrait.php
index bd1ee75..ba5808b 100644
--- a/core/lib/Drupal/Component/Utility/SafeStringTrait.php
+++ b/core/lib/Drupal/Component/Utility/SafeStringTrait.php
@@ -54,6 +54,22 @@ public static function create($string) {
    *   The safe string content.
    */
   public function __toString() {
+    try {
+      return $this->toString();
+    }
+    catch (\Exception $e) {
+      trigger_error('An exception was thrown while calling __toString on a ' . gettype($this) . ' in ' . $e->getFile() . ' on line ' . $e->getLine() . ': ' . $e->getMessage());
+      return '';
+    }
+  }
+
+  /**
+   * Returns the string version of the SafeString object.
+   *
+   * @return string
+   *   The safe string content.
+   */
+  public function toString() {
     return $this->string;
   }
 
diff --git a/core/lib/Drupal/Component/Utility/ToStringConverter.php b/core/lib/Drupal/Component/Utility/ToStringConverter.php
new file mode 100644
index 0000000..ad8193c
--- /dev/null
+++ b/core/lib/Drupal/Component/Utility/ToStringConverter.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Utility\ToStringConverter.
+ */
+
+namespace Drupal\Component\Utility;
+
+/**
+ * Converts objects to strings.
+ */
+class ToStringConverter {
+
+  /**
+   * Converts an object into a string.
+   *
+   * @param object $object
+   *   The object to convert.
+   * @param bool $fallback
+   *   Whether to fall back to toString() for objects that are not instances of
+   *   ToStringInterface (optional). This is an internal variable that is
+   *   set to TRUE in the renderer for backward compatibility reasons but this
+   *   should remain FALSE everywhere else.
+   *
+   * @return string
+   *   The string.
+   *
+   * @throws \Exception
+   *
+   * @todo deprecate $fallback for Drupal 9 and enforce ToStringInterface.
+   */
+  public static function convert($object, $fallback = FALSE) {
+    if ($object instanceof ToStringInterface) {
+      // You can't throw exceptions in the magic PHP __toString methods, so
+      // we also support a toString method.
+      // @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
+      return $object->toString();
+    }
+    if (method_exists($object, '__toString')) {
+      // We prefer __toString() on objects that are not instances of
+      // ToStringInterface.
+      return (string) $object;
+    }
+    if ($fallback && method_exists($object, 'toString')) {
+      return $object->toString();
+    }
+    throw new \Exception('Object of type "' . get_class($object) . '" cannot be printed.');
+  }
+
+}
diff --git a/core/lib/Drupal/Component/Utility/ToStringInterface.php b/core/lib/Drupal/Component/Utility/ToStringInterface.php
new file mode 100644
index 0000000..6e5829d
--- /dev/null
+++ b/core/lib/Drupal/Component/Utility/ToStringInterface.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\Utility\ToStringInterface
+ */
+
+namespace Drupal\Component\Utility;
+
+/**
+ * Marks an object as having a special toString() method.
+ *
+ * @see \Drupal\Component\Utility\StringConverter::convert()
+ */
+interface ToStringInterface {
+
+  /**
+   * Converts an object into a string.
+   *
+   * Same as the __toString() magic method but with less fatal errors.
+   *
+   * @return string
+   *   The string.
+   */
+  public function toString();
+
+}
diff --git a/core/lib/Drupal/Core/Link.php b/core/lib/Drupal/Core/Link.php
index 05869a2..3efa73a 100644
--- a/core/lib/Drupal/Core/Link.php
+++ b/core/lib/Drupal/Core/Link.php
@@ -7,12 +7,13 @@
 
 namespace Drupal\Core;
 
+use Drupal\Component\Utility\ToStringInterface;
 use Drupal\Core\Routing\LinkGeneratorTrait;
 
 /**
  * Defines an object that holds information about a link.
  */
-class Link {
+class Link implements ToStringInterface {
 
   use LinkGeneratorTrait;
 
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index bb8f1ff..33cf1a5 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Render;
 
 use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\ToStringConverter;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Component\Utility\Xss;
@@ -121,6 +122,23 @@ public function __construct(ControllerResolverInterface $controller_resolver, Th
   }
 
   /**
+   * Convert an argument to a string or leave it alone.
+   *
+   * @param mixed $arg
+   *   If it is an object, it will be converted to a string, otherwise it will
+   *   be left alone.
+   *
+   * @return mixed
+   *   If $arg was an object, this will be a string, otherwise this will be
+   *   be the unchanged input value $arg.
+   *
+   * @throws \Exception
+   */
+  protected function convert($arg) {
+    return is_object($arg) ? ToStringConverter::convert($arg) : $arg;
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function renderRoot(&$elements) {
@@ -453,7 +471,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     // same process as Renderer::render() but is inlined for speed.
     if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
       foreach ($children as $key) {
-        $elements['#children'] .= $this->doRender($elements[$key]);
+        $elements['#children'] .= $this->convert($this->doRender($elements[$key]));
       }
       $elements['#children'] = SafeString::create($elements['#children']);
     }
@@ -466,7 +484,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     // required. Eventually #theme_wrappers will expect both #markup and
     // #children to be a single string as #children.
     if (!$theme_is_implemented && isset($elements['#markup'])) {
-      $elements['#children'] = SafeString::create($elements['#markup'] . $elements['#children']);
+      $elements['#children'] = SafeString::create($this->convert($elements['#markup']) . $this->convert($elements['#children']));
     }
 
     // Let the theme functions in #theme_wrappers add markup around the rendered
@@ -516,10 +534,10 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     // with how render cached output gets stored. This ensures that placeholder
     // replacement logic gets the same data to work with, no matter if #cache is
     // disabled, #cache is enabled, there is a cache hit or miss.
-    $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : '';
-    $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : '';
+    $prefix = isset($elements['#prefix']) ? $this->convert($this->xssFilterAdminIfUnsafe($elements['#prefix'])) : '';
+    $suffix = isset($elements['#suffix']) ? $this->convert($this->xssFilterAdminIfUnsafe($elements['#suffix'])) : '';
 
-    $elements['#markup'] = SafeString::create($prefix . $elements['#children'] . $suffix);
+    $elements['#markup'] = SafeString::create($prefix . $this->convert($elements['#children']) . $suffix);
 
     // We've rendered this element (and its subtree!), now update the context.
     $context->update($elements);
diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
index dffd2cf..5f47967 100644
--- a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
+++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php
@@ -98,6 +98,13 @@ public function getOptions() {
    * Implements the magic __toString() method.
    */
   public function __toString() {
+    return $this->toString();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function toString() {
     return $this->render();
   }
 
diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php
index cc0d591..0162412 100644
--- a/core/lib/Drupal/Core/Template/Attribute.php
+++ b/core/lib/Drupal/Core/Template/Attribute.php
@@ -253,6 +253,13 @@ public function hasClass($class) {
    * Implements the magic __toString() method.
    */
   public function __toString() {
+    return $this->toString();
+  }
+
+  /**
+   * @inheritdoc
+   */
+  public function toString() {
     $return = '';
     /** @var \Drupal\Core\Template\AttributeValueBase $value */
     foreach ($this->storage as $name => $value) {
diff --git a/core/lib/Drupal/Core/Template/TwigExtension.php b/core/lib/Drupal/Core/Template/TwigExtension.php
index b06f66b..120b4ae 100644
--- a/core/lib/Drupal/Core/Template/TwigExtension.php
+++ b/core/lib/Drupal/Core/Template/TwigExtension.php
@@ -15,6 +15,7 @@
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\SafeStringInterface;
+use Drupal\Component\Utility\ToStringConverter;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\Theme\ThemeManagerInterface;
@@ -445,34 +446,19 @@ public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $
    * @see TwigNodeVisitor
    */
   public function renderVar($arg) {
-    // Check for a numeric zero int or float.
-    if ($arg === 0 || $arg === 0.0) {
+    if ($arg === 0 || $arg === "0" || $arg === 0.0) {
+      // See https://www.drupal.org/node/2417733 about 0.0
       return 0;
     }
-
-    // Return early for NULL and empty arrays.
-    if ($arg == NULL) {
+    // Return early for empty arguments.
+    if (empty($arg)) {
       return NULL;
     }
-
-    // Optimize for scalars as it is likely they come from the escape filter.
-    if (is_scalar($arg)) {
-      return $arg;
-    }
-
     if (is_object($arg)) {
-      if (method_exists($arg, '__toString')) {
-        return (string) $arg;
-      }
-      // You can't throw exceptions in the magic PHP __toString methods, see
-      // http://php.net/manual/en/language.oop5.magic.php#object.tostring so
-      // we also support a toString method.
-      elseif (method_exists($arg, 'toString')) {
-        return $arg->toString();
-      }
-      else {
-        throw new \Exception(t('Object of type "@class" cannot be printed.', array('@class' => get_class($arg))));
-      }
+      $arg = ToStringConverter::convert($arg, TRUE);
+    }
+    if (!is_array($arg)) {
+      return $arg;
     }
 
     // This is a render array, with special simple cases already handled.
diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php
index 5da8ffb8..9465f11 100644
--- a/core/lib/Drupal/Core/Url.php
+++ b/core/lib/Drupal/Core/Url.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core;
 
+use Drupal\Component\Utility\ToStringInterface;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -19,7 +20,7 @@
 /**
  * Defines an object that holds information about a URL.
  */
-class Url {
+class Url implements ToStringInterface {
   use DependencySerializationTrait;
 
   /**
