diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php
index 104fecfbc72976d9cc3ed9508c53f53d7be5654f..234af008734a0f2dad055f1ee2c44094fc83b535 100644
--- a/core/lib/Drupal/Core/Database/Connection.php
+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -495,13 +495,14 @@ protected function filterComment($comment = '') {
    * @return Drupal\Core\Database\StatementInterface
    *   This method will return one of: the executed statement, the number of
    *   rows affected by the query (not the number matched), or the generated
-   *   insert IT of the last query, depending on the value of
+   *   insert ID of the last query, depending on the value of
    *   $options['return']. Typically that value will be set by default or a
    *   query builder and should not be set by a user. If there is an error,
    *   this method will return NULL and may throw an exception if
    *   $options['throw_exception'] is TRUE.
    *
    * @throws PDOException
+   * @throws Drupal\Core\Database\IntegrityConstraintViolationException
    */
   public function query($query, array $args = array(), $options = array()) {
 
@@ -545,7 +546,13 @@ public function query($query, array $args = array(), $options = array()) {
         // debug information.
         $query_string = ($query instanceof DatabaseStatementInterface) ? $stmt->getQueryString() : $query;
         $message = $e->getMessage() . ": " . $query_string . "; " . print_r($args, TRUE);
-        $exception = new DatabaseExceptionWrapper($message, 0, $e);
+        // Match all SQLSTATE 23xxx errors.
+        if (substr($e->getCode(), -6, -3) == '23') {
+          $exception = new IntegrityConstraintViolationException($message, $e->getCode(), $e);
+        }
+        else {
+          $exception = new DatabaseExceptionWrapper($message, 0, $e);
+        }
 
         throw $exception;
       }
diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
index c2780049fb83b53c32b16f75e2548aac57fb5580..4fd7d0d4c803fe1456cbddf2741b4643afb9c504 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Database\Connection as DatabaseConnection;
 use Drupal\Core\Database\DatabaseNotFoundException;
 use Drupal\Core\Database\StatementInterface;
+use Drupal\Core\Database\IntegrityConstraintViolationException;
 
 use Locale;
 use PDO;
@@ -132,6 +133,10 @@ public function query($query, array $args = array(), $options = array()) {
     }
     catch (PDOException $e) {
       if ($options['throw_exception']) {
+        // Match all SQLSTATE 23xxx errors.
+        if (substr($e->getCode(), -6, -3) == '23') {
+          $e = new IntegrityConstraintViolationException($e->getMessage(), $e->getCode(), $e);
+        }
         // Add additional debug information.
         if ($query instanceof StatementInterface) {
           $e->query_string = $stmt->getQueryString();
diff --git a/core/lib/Drupal/Core/Database/IntegrityConstraintViolationException.php b/core/lib/Drupal/Core/Database/IntegrityConstraintViolationException.php
new file mode 100644
index 0000000000000000000000000000000000000000..7b8ac43957de1fa8d5898b261952edb0695bd919
--- /dev/null
+++ b/core/lib/Drupal/Core/Database/IntegrityConstraintViolationException.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Core\Database\IntegrityConstraintViolationException
+ */
+
+namespace Drupal\Core\Database;
+
+use RuntimeException;
+
+/**
+ * Exception thrown if a query would violate an integrity constraint.
+ *
+ * This exception is thrown e.g. when trying to insert a row that would violate
+ * a unique key constraint.
+ */
+class IntegrityConstraintViolationException extends RuntimeException implements DatabaseException { }
diff --git a/core/lib/Drupal/Core/Database/Query/Merge.php b/core/lib/Drupal/Core/Database/Query/Merge.php
index 547a22323b2d90bfa52205378a7d0a0c22b4598c..3d7564afe173bc2bb25ed86894deae9bedb13859 100644
--- a/core/lib/Drupal/Core/Database/Query/Merge.php
+++ b/core/lib/Drupal/Core/Database/Query/Merge.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Database\Database;
 use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\IntegrityConstraintViolationException;
 
 use Exception;
 
@@ -423,7 +424,7 @@ public function execute() {
           $insert->execute();
           return self::STATUS_INSERT;
         }
-        catch (Exception $e) {
+        catch (IntegrityConstraintViolationException $e) {
           // The insert query failed, maybe it's because a racing insert query
           // beat us in inserting the same row. Retry the select query, if it
           // returns a row, ignore the error and continue with the update
diff --git a/core/lib/Drupal/Core/Lock/DatabaseLockBackend.php b/core/lib/Drupal/Core/Lock/DatabaseLockBackend.php
index d877dbab585a5c95e70e569e15f8f4183fecd4ad..b0ee0a3a6e29ee1c71430d27c0caec8fd6f5c499 100644
--- a/core/lib/Drupal/Core/Lock/DatabaseLockBackend.php
+++ b/core/lib/Drupal/Core/Lock/DatabaseLockBackend.php
@@ -7,7 +7,7 @@
 
 namespace Drupal\Core\Lock;
 
-use Drupal\Core\Database\DatabaseExceptionWrapper;
+use Drupal\Core\Database\IntegrityConstraintViolationException;
 
 /**
  * Defines the database lock backend. This is the default backend in Drupal.
@@ -53,12 +53,12 @@ public function acquire($name, $timeout = 30.0) {
           // We never need to try again.
           $retry = FALSE;
         }
-        catch (DatabaseExceptionWrapper $e) {
+        catch (IntegrityConstraintViolationException $e) {
           // Suppress the error. If this is our first pass through the loop,
-          // then $retry is FALSE. In this case, the insert must have failed
-          // meaning some other request acquired the lock but did not release it.
-          // We decide whether to retry by checking lock_may_be_available()
-          // Since this will break the lock in case it is expired.
+          // then $retry is FALSE. In this case, the insert failed because some
+          // other request acquired the lock but did not release it. We decide
+          // whether to retry by checking lockMayBeAvailable(). This will clear
+          // the offending row from the database table in case it has expired.
           $retry = $retry ? FALSE : $this->lockMayBeAvailable($name);
         }
         // We only retry in case the first attempt failed, but we then broke
diff --git a/core/modules/system/lib/Drupal/system/Tests/Database/InvalidDataTest.php b/core/modules/system/lib/Drupal/system/Tests/Database/InvalidDataTest.php
index 90a9e18a150d53e1a521f1355cf8897423076604..c60b6d0b2d4e499d143852e31206e25908009030 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Database/InvalidDataTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Database/InvalidDataTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Database;
 
+use Drupal\Core\Database\IntegrityConstraintViolationException;
 use Exception;
 
 /**
@@ -46,7 +47,7 @@ function testInsertDuplicateData() {
         ->execute();
       $this->fail('Insert succeedded when it should not have.');
     }
-    catch (Exception $e) {
+    catch (IntegrityConstraintViolationException $e) {
       // Check if the first record was inserted.
       $name = db_query('SELECT name FROM {test} WHERE age = :age', array(':age' => 63))->fetchField();