diff --git a/core/core.services.yml b/core/core.services.yml
index 5877a38..88f026f 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.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/DatabaseNotFoundException.php b/core/lib/Drupal/Core/Database/DatabaseAccessDeniedException.php
similarity index 52%
copy from core/lib/Drupal/Core/Database/DatabaseNotFoundException.php
copy to core/lib/Drupal/Core/Database/DatabaseAccessDeniedException.php
index 63eb9a3..a797fea 100644
--- a/core/lib/Drupal/Core/Database/DatabaseNotFoundException.php
+++ b/core/lib/Drupal/Core/Database/DatabaseAccessDeniedException.php
@@ -5,4 +5,4 @@
 /**
  * Exception thrown if specified database is not found.
  */
-class DatabaseNotFoundException extends \RuntimeException {}
+class DatabaseAccessDeniedException extends \RuntimeException implements DatabaseException {}
diff --git a/core/lib/Drupal/Core/Database/DatabaseNotFoundException.php b/core/lib/Drupal/Core/Database/DatabaseNotFoundException.php
index 63eb9a3..1c5b5eb 100644
--- a/core/lib/Drupal/Core/Database/DatabaseNotFoundException.php
+++ b/core/lib/Drupal/Core/Database/DatabaseNotFoundException.php
@@ -5,4 +5,4 @@
 /**
  * Exception thrown if specified database is not found.
  */
-class DatabaseNotFoundException extends \RuntimeException {}
+class DatabaseNotFoundException extends \RuntimeException implements DatabaseException {}
diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
index df51067..fd46438 100644
--- a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
+++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Database\Driver\mysql;
 
+use Drupal\Core\Database\DatabaseAccessDeniedException;
 use Drupal\Core\Database\DatabaseExceptionWrapper;
 
 use Drupal\Core\Database\Database;
@@ -27,6 +28,11 @@ class Connection extends DatabaseConnection {
   const DATABASE_NOT_FOUND = 1049;
 
   /**
+   * Error code for "Access denied" error.
+   */
+  const ACCESS_DENIED = 1045;
+
+  /**
    * Error code for "Can't initialize character set" error.
    */
   const UNSUPPORTED_CHARSET = 2019;
@@ -139,7 +145,18 @@ public static function open(array &$connection_options = array()) {
       $connection_options['pdo'] += [\PDO::MYSQL_ATTR_MULTI_STATEMENTS => FALSE];
     }
 
-    $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
+    try {
+      $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
+    }
+    catch (\PDOException $e) {
+      if ($e->getCode() == static::DATABASE_NOT_FOUND) {
+        throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
+      }
+      if ($e->getCode() == static::ACCESS_DENIED) {
+        throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e);
+      }
+      throw $e;
+    }
 
     // Force MySQL to use the UTF-8 character set. Also set the collation, if a
     // certain one has been set; otherwise, MySQL defaults to
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
index 4688cde..042c221 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Database\Database;
 use Drupal\Core\Database\Connection as DatabaseConnection;
+use Drupal\Core\Database\DatabaseAccessDeniedException;
 use Drupal\Core\Database\DatabaseNotFoundException;
 
 /**
@@ -27,6 +28,13 @@ class Connection extends DatabaseConnection {
   const DATABASE_NOT_FOUND = 7;
 
   /**
+   * Error code for "Access denied" error.
+   *
+   * Technically the error message is "insufficient_privilege".
+   */
+  const ACCESS_DENIED = '42501';
+
+  /**
    * The list of PostgreSQL reserved key words.
    *
    * @see http://www.postgresql.org/docs/9.4/static/sql-keywords-appendix.html
@@ -113,7 +121,19 @@ public static function open(array &$connection_options = array()) {
       // Convert numeric values to strings when fetching.
       \PDO::ATTR_STRINGIFY_FETCHES => TRUE,
     );
-    $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
+
+    try {
+      $pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
+    }
+    catch (\PDOException $e) {
+      if ($e->getCode() == static::DATABASE_NOT_FOUND) {
+        throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
+      }
+      if ($e->getCode() == static::ACCESS_DENIED) {
+        throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e);
+      }
+      throw $e;
+    }
 
     return $pdo;
   }
diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php
index 2f5374b..10c63e8 100644
--- a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php
@@ -99,7 +99,19 @@ public static function open(array &$connection_options = array()) {
       // Convert numeric values to strings when fetching.
       \PDO::ATTR_STRINGIFY_FETCHES => TRUE,
     );
-    $pdo = new \PDO('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']);
+
+    try {
+      $pdo = new \PDO('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']);
+    }
+    catch (\PDOException $e) {
+      if ($e->getCode() == static::DATABASE_NOT_FOUND) {
+        throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
+      }
+      // SQLite doesn't have a distinct error code for access denied, so don't
+      // deal with that case.
+      throw $e;
+    }
+
 
     // Create functions needed by SQLite.
     $pdo->sqliteCreateFunction('if', array(__CLASS__, 'sqlFunctionIf'));
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 1a3b943..db2df4f5 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.
@@ -621,7 +623,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();
@@ -660,14 +662,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..4fe8af8
--- /dev/null
+++ b/core/lib/Drupal/Core/Installer/CheckInstalledTrait.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Drupal\Core\Installer;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\DatabaseAccessDeniedException;
+use Drupal\Core\Database\DatabaseException;
+use Drupal\Core\Database\DatabaseNotFoundException;
+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;
+    }
+
+    // If the database wasn't found, assume the user hasn't entered it properly
+    // and redirect to the installer.
+    if ($exception instanceof DatabaseNotFoundException) {
+      return TRUE;
+    }
+
+    // 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) {
+      try {
+        return !$connection->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). Assume we shouldn't redirect, just in case.
+        return FALSE;
+      }
+    }
+
+    // When in doubt, don't redirect.
+    return FALSE;
+  }
+}
