diff --git a/core/core.services.yml b/core/core.services.yml
index 5877a38..6f6046d 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1169,6 +1169,11 @@ services:
     tags:
       - { name: event_subscriber }
     arguments: ['@config.manager', '@config.storage', '@config.storage.snapshot']
+  exception.neds_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/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 1a3b943..a0adb9e 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -9,6 +9,7 @@
 use Drupal\Core\Config\BootstrapConfigStorageFactory;
 use Drupal\Core\Config\NullStorage;
 use Drupal\Core\Database\Database;
+use Drupal\Core\Database\DatabaseException;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DependencyInjection\ServiceModifierInterface;
 use Drupal\Core\DependencyInjection\ServiceProviderInterface;
@@ -621,7 +622,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();
@@ -665,9 +666,49 @@ protected function handleException(\Exception $e, $request, $type) {
       $response->headers->add($e->getHeaders());
       return $response;
     }
-    else {
-      throw $e;
+
+    if (PHP_SAPI !== 'cli' && $this->isNotInstalledException($e)) {
+      return new RedirectResponse($request->getBasePath() . '/core/install.php', 302, ['Cache-Control' => 'no-cache']);
     }
+
+    throw $e;
+  }
+
+  /**
+   * Determines if the exception is for a missing or not installed database.
+   *
+   * @param \Exception $e
+   *   The exception to check.
+   *
+   * @return bool
+   *   TRUE if the exception is for a missing or not fully installed database,
+   *   FALSE otherwise.
+   */
+  protected function isNotInstalledException(\Exception $e) {
+    if (!($e instanceof \PDOException || $e instanceof DatabaseException)) {
+      return FALSE;
+    }
+
+    // If we're already in the installer, don't worry about Drupal not being
+    // installed.
+    if (drupal_installation_attempted()) {
+      return FALSE;
+    }
+
+    // Code 1049 is "missing database".
+    if ($e->getCode() == 1049) {
+      return TRUE;
+    }
+
+    // SQLSTATE[42S02] indicates a missing table. Since we should only get here
+    // in case a bootstrap step failed, we assume that whatever table is missing
+    // is missing because the installation is broken, incomplete, or not even
+    // started.
+    if ($e->getCode() == 0 && strpos($e->getMessage(), 'SQLSTATE[42S02]') !== FALSE) {
+      return TRUE;
+    }
+
+    return FALSE;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionDetectNeedsInstallSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionDetectNeedsInstallSubscriber.php
new file mode 100644
index 0000000..0a9f58d
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionDetectNeedsInstallSubscriber.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Database\Connection;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+
+/**
+ * Exception handler to determine if an exception indicates an uninstalled site.
+ *
+ * Note: The technique here assumes Drupal is installed using SQL, and that
+ * sessions are being stored in the database.  If sessions are stored elsewhere
+ * or Drupal is being run entirely non-SQL then this detection mechanism may
+ * fail.  In such environments it may be necessary to disable this service
+ * via the container.
+ */
+class ExceptionDetectNeedsInstallSubscriber extends HttpExceptionSubscriberBase {
+
+  /**
+   * The default database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  public function __construct(Connection $connection) {
+    $this->connection = $connection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getHandledFormats() {
+    return ['html'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static function getPriority() {
+    return 100;
+  }
+
+  /**
+   * Determines if Drupal still needs to be installed.
+   *
+   * @return bool
+   *   TRUE if the system still needs to be installed, FALSE otherwise.
+   */
+  protected function systemNeedsInstall() {
+    // If we're already in the installer, don't worry about Drupal not being
+    // installed.
+    if (PHP_SAPI === 'cli' || drupal_installation_attempted()) {
+      return FALSE;
+    }
+
+    // If there's no sessions table, we assume it means Drupal was never
+    // installed. This may be an incorrect assumption on non-SQL-based
+    // installations.
+    return !$this->connection->schema()->tableExists('session');
+  }
+
+  /**
+   * Checks if a 404 error is really a sign of an uninstalled Drupal.
+   *
+   * @todo Remove this method in favor of on400() once
+   * https://www.drupal.org/node/2739617 is committed.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   */
+  public function on404(GetResponseForExceptionEvent $event) {
+    return $this->on400($event);
+  }
+
+  /**
+   * Checks if a 404 error is really a sign of an uninstalled Drupal.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *
+   */
+  public function on400(GetResponseForExceptionEvent $event) {
+    if ($this->systemNeedsInstall()) {
+      $event->setResponse(new RedirectResponse($event->getRequest()->getBasePath() . '/core/install.php', 302, ['Cache-Control' => 'no-cache']));
+    }
+  }
+}
