diff --git a/core/core.services.yml b/core/core.services.yml
index 1ee07fe..b7507aa 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1172,6 +1172,11 @@ services:
     tags:
       - { name: event_subscriber }
     arguments: ['@config.manager', '@config.storage', '@config.storage.snapshot']
+  exception.needs_installer:
+    class: Drupal\Core\EventSubscriber\ExceptionDetectNeedsInstallSubscriber
+    arguments: ['@database']
+    tags:
+      - { name: event_subscriber }
   exception.default_json:
     class: Drupal\Core\EventSubscriber\ExceptionJsonSubscriber
     tags:
diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php
index 791bf7a..08d64ad 100644
--- a/core/lib/Drupal/Core/Database/Connection.php
+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -1419,6 +1419,33 @@ public function quote($string, $parameter_type = \PDO::PARAM_STR) {
   }

   /**
+   * Checks if there the database is empty.
+   *
+   * @return bool
+   *   Returns TRUE if there is an empty database for the connection.
+   */
+  public function databaseEmpty() {
+    // Check for the presence of a database table provided in the hook_schema()
+    // implementation of a required core module. That should always be present
+    // if Drupal is already installed.
+    try {
+      return !$this->schema()->tableExists('sessions');
+    }
+    catch (\Exception $e) {
+      // If we still have an exception at this point, we need to be careful
+      // since we should not redirect if the exception represents an error on an
+      // already-installed site (for example, if the database server went down).
+      // However a database error with code 1049 means "missing database", and
+      // it should be safe to redirect in that case.
+      return ($e instanceof \PDOException || $e instanceof DatabaseException) && $e->getCode() == 1049;
+    }
+
+    // Default to FALSE to minimize the chance of overwriting an installed
+    // database.
+    return FALSE;
+  }
+
+  /**
    * Prevents the database connection from being serialized.
    */
   public function __sleep() {
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 1c8068b..e39adde 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -16,6 +16,7 @@
 use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\File\MimeType\MimeTypeGuesser;
 use Drupal\Core\Http\TrustedHostsRequestFactory;
+use Drupal\Core\Installer\CheckInstalledTrait;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Site\Settings;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
@@ -44,6 +45,7 @@
  * container, or modify existing services.
  */
 class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
+  use CheckInstalledTrait;

   /**
    * Holds the class used for dumping the container to a PHP array.
@@ -647,7 +649,7 @@ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch =
       // installed yet (i.e., if no $databases array has been defined in the
       // settings.php file) and we are not already installing.
       if (!Database::getConnectionInfo() && !drupal_installation_attempted() && PHP_SAPI !== 'cli') {
-        $response = new RedirectResponse($request->getBasePath() . '/core/install.php');
+        $response = new RedirectResponse($request->getBasePath() . '/core/install.php', 302, ['Cache-Control' => 'no-cache']);
       }
       else {
         $this->boot();
@@ -686,14 +688,17 @@ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch =
    *   If the passed in exception cannot be turned into a response.
    */
   protected function handleException(\Exception $e, $request, $type) {
+    if ($this->shouldRedirectToInstaller($e, $this->container ? $this->container->get('database') : NULL)) {
+      return new RedirectResponse($request->getBasePath() . '/core/install.php', 302, ['Cache-Control' => 'no-cache']);
+    }
+
     if ($e instanceof HttpExceptionInterface) {
       $response = new Response($e->getMessage(), $e->getStatusCode());
       $response->headers->add($e->getHeaders());
       return $response;
     }
-    else {
-      throw $e;
-    }
+
+    throw $e;
   }

   /**
diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionDetectNeedsInstallSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionDetectNeedsInstallSubscriber.php
new file mode 100644
index 0000000..b94a0dc
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionDetectNeedsInstallSubscriber.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Installer\CheckInstalledTrait;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Exception handler to determine if an exception indicates an uninstalled site.
+ */
+class ExceptionDetectNeedsInstallSubscriber implements EventSubscriberInterface {
+  use CheckInstalledTrait;
+
+  /**
+   * The default database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Constructs a new ExceptionDetectNeedsInstallSubscriber.
+   *
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The default database connection.
+   */
+  public function __construct(Connection $connection) {
+    $this->connection = $connection;
+  }
+
+  /**
+   * Handles errors for this subscriber.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   */
+  public function onException(GetResponseForExceptionEvent $event) {
+    $exception = $event->getException();
+    if ($this->shouldRedirectToInstaller($exception, $this->connection)) {
+      // Only redirect if this is an HTML response (i.e., a user trying to view
+      // the site in a web browser before installing it).
+      $request = $event->getRequest();
+      $format = $request->query->get(MainContentViewSubscriber::WRAPPER_FORMAT, $request->getRequestFormat());
+      if ($format == 'html') {
+        $event->setResponse(new RedirectResponse($request->getBasePath() . '/core/install.php', 302, ['Cache-Control' => 'no-cache']));
+      }
+    }
+  }
+
+  /**
+   * Registers the methods in this class that should be listeners.
+   *
+   * @return array
+   *   An array of event listener definitions.
+   */
+  public static function getSubscribedEvents() {
+    $events[KernelEvents::EXCEPTION][] = ['onException', 100];
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Installer/CheckInstalledTrait.php b/core/lib/Drupal/Core/Installer/CheckInstalledTrait.php
new file mode 100644
index 0000000..f5fa91d
--- /dev/null
+++ b/core/lib/Drupal/Core/Installer/CheckInstalledTrait.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\Core\Installer;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\DatabaseException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Provides methods for checking if Drupal is already installed.
+ */
+trait CheckInstalledTrait {
+
+  /**
+   * Determines if an exception handler should redirect to the installer.
+   *
+   * @param \Exception $exception
+   *   The exception to check.
+   * @param \Drupal\Core\Database\Connection|null $connection
+   *   (optional) The default database connection. If not provided, a less
+   *   comprehensive check will be performed. This can be the case if the
+   *   exception occurs early enough that a database connection object isn't
+   *   available from the container yet.
+   *
+   * @return bool
+   *   TRUE if the exception handler should redirect to the installer because
+   *   Drupal is not installed yet, or FALSE otherwise.
+   */
+  protected function shouldRedirectToInstaller(\Exception $exception, Connection $connection = NULL) {
+    // To avoid unnecessary queries, only act if the exception is one that is
+    // expected to occur when Drupal has not yet been installed. This includes
+    // NotFoundHttpException because an uninstalled site won't have route
+    // information available yet and therefore can return 404 errors.
+    if (!($exception instanceof \PDOException || $exception instanceof DatabaseException || $exception instanceof NotFoundHttpException)) {
+      return FALSE;
+    }
+
+    // Never redirect on the command line.
+    if (PHP_SAPI === 'cli') {
+      return FALSE;
+    }
+
+    // Never redirect if we're already in the installer.
+    if (drupal_installation_attempted()) {
+      return FALSE;
+    }
+
+    // Redirect if there isn't even any database connection information in
+    // settings.php yet, since that means Drupal is not installed.
+    if (!Database::getConnectionInfo()) {
+      return TRUE;
+    }
+
+    // Redirect if the database is empty.
+    if ($connection) {
+      return $connection->databaseEmpty();
+    }
+  }
+
+}
