Repository: cloudant/python-cloudant

Issue:
Improve CloudantException design
Using the ResultException class as a guide, we can improve the error handling design by 1) have specific class exceptions for many or all of the modules (i.e. `CloudantClientException` and 2) move the error messages from `error.py` and the other modules to an internal `messages.py` module to reduce clutter.

After a discussion with @alfinkel, will follow these guidelines:
(1) try and preserve Python `dict` behavior
(2) only throwing Cloudant exceptions when trying to communicate errors encountered pre-remote database interaction 



Discussion:
emlaver: @smithsz @alfinkel I'm considering changing the exception class names to module+exception i.e. from `CloudantClientException` to `ClientException`.  Any thoughts?
alfinkel: Have you considered doing something similar to this with the CloudantArgumentError expections?
emlaver: @alfinkel I've considered moving the `CloudantArgumentError` error messages to the `messages` module.  I currently don't see it being worthwhile to have separate classes for argument errors in each module.
alfinkel: I agree that it's not worthwhile splitting the argument errors exceptions into their own classes.  However, my question was whether you considered moving _all_ argument error messages into your `_messages` module and basically handling argument error messages the same way as you handle the individual class exception messages.

Patch:
diff --git a/src/cloudant/_messages.py b/src/cloudant/_messages.py
new file mode 100644
index 0000000..79485ad
--- /dev/null
+++ b/src/cloudant/_messages.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# Copyright (c) 2016 IBM. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Module that contains exception messages for the Cloudant Python client
+library.
+"""
+CLIENT = {
+    100: 'A general Cloudant client exception was raised.',
+    101: 'Value must be set to a Database object. Found type: {0}.',
+    102: 'You must provide a url or an account.',
+    404: 'Database {0} does not exist. Verify that the client is valid and try again.',
+    409: 'Database {0} already exists.'
+}
+
+DATABASE = {
+    100: 'A general Cloudant database exception was raised.',
+    101: 'Unexpected index type. Found: {0}.',
+    400: 'Invalid database name during creation. Found: {0}',
+    401: 'Unauthorized to create database {0}.',
+    409: 'Document with id {0} already exists.'
+}
+
+DESIGN_DOCUMENT = {
+    100: 'A general Cloudant design document exception was raised.',
+    101: 'Cannot add a MapReduce view to a design document for query indexes.',
+    102: 'Cannot update a query index view using this method.',
+    103: 'Cannot delete a query index view using this method.',
+    104: 'View {0} must be of type View.',
+    105: 'View {0} must be of type QueryIndexView.',
+    106: 'Function for search index {0} must be of type string.',
+    107: 'Definition for query text index {0} must be of type dict.'
+}
+
+DOCUMENT = {
+    100: 'A general Cloudant document exception was raised.',
+    101: 'A document id is required to fetch document contents. '
+         'Add an _id key and value to the document and re-try.',
+    102: 'The field {0} is not a list.',
+    103: 'Attempting to delete a doc with no _rev. Try running .fetch and re-try.'
+}
+
+FEED = {
+    100: 'A general Cloudant feed exception was raised.',
+    101: 'Infinite _db_updates feed not supported for CouchDB.'
+}
+
+INDEX = {
+    100: 'A general Cloudant index exception was raised.',
+    101: 'Creating the \"special\" index is not allowed.',
+    102: 'Deleting the \"special\" index is not allowed.'
+}
+
+REPLICATOR = {
+    100: 'A general Cloudant replicator exception was raised.',
+    101: 'You must specify either a source_db Database object or a manually composed'
+         ' \'source\' string/dict.',
+    102: 'You must specify either a target_db Database object or a manually composed'
+         ' \'target\' string/dict.',
+    404: 'Replication with id {0} not found.'
+}
+
+RESULT = {
+    100: 'A general result exception was raised.',
+    101: 'Failed to interpret the argument {0} as a valid key value or as a valid slice.',
+    102: 'Cannot use {0} when performing key access or key slicing. Found {1}.',
+    103: 'Cannot use {0} for iteration. Found {1}.',
+    104: 'Invalid page_size: {0}'
+}
diff --git a/src/cloudant/client.py b/src/cloudant/client.py
index ea4fe7f..7d73d5c 100755
--- a/src/cloudant/client.py
+++ b/src/cloudant/client.py
@@ -24,7 +24,7 @@ import requests
 from ._2to3 import bytes_, unicode_
 from .database import CloudantDatabase, CouchDatabase
 from .feed import Feed, InfiniteFeed
-from .error import CloudantException, CloudantArgumentError
+from .error import CloudantArgumentError, CloudantClientException
 from ._common_util import (
     USER_AGENT,
     append_response_error_content,
@@ -198,37 +198,34 @@ class CouchDB(dict):
         Creates a new database on the remote server with the name provided
         and adds the new database object to the client's locally cached
         dictionary before returning it to the caller.  The method will
-        optionally throw a CloudantException if the database exists remotely.
+        optionally throw a CloudantClientException if the database
+        exists remotely.
 
         :param str dbname: Name used to create the database.
         :param bool throw_on_exists: Boolean flag dictating whether or
-            not to throw a CloudantException when attempting to create a
-            database that already exists.
+            not to throw a CloudantClientException when attempting to
+            create a database that already exists.
 
         :returns: The newly created database object
         """
         new_db = self._DATABASE_CLASS(self, dbname)
         if new_db.exists():
             if kwargs.get('throw_on_exists', True):
-                raise CloudantException(
-                    "Database {0} already exists".format(dbname)
-                )
+                raise CloudantClientException(409, dbname)
         new_db.create()
         super(CouchDB, self).__setitem__(dbname, new_db)
         return new_db
 
     def delete_database(self, dbname):
         """
-        Removes the named database remotely and locally. The method will throw a
-        CloudantException if the database does not exist.
+        Removes the named database remotely and locally. The method will throw
+        a CloudantClientException if the database does not exist.
 
         :param str dbname: Name of the database to delete.
         """
         db = self._DATABASE_CLASS(self, dbname)
         if not db.exists():
-            raise CloudantException(
-                "Database {0} does not exist".format(dbname)
-            )
+            raise CloudantClientException(404, dbname)
         db.delete()
         if dbname in list(self.keys()):
             super(CouchDB, self).__delitem__(dbname)
@@ -378,8 +375,7 @@ class CouchDB(dict):
             create the database remotely or not.  Defaults to False.
         """
         if not isinstance(value, self._DATABASE_CLASS):
-            msg = "Value must be set to a Database object"
-            raise CloudantException(msg)
+            raise CloudantClientException(101, type(value).__name__)
         if remote and not value.exists():
             value.create()
         super(CouchDB, self).__setitem__(key, value)
@@ -426,7 +422,7 @@ class Cloudant(CouchDB):
                 self._client_user_header['X-Cloudant-User'] = x_cloudant_user
 
         if self.server_url is None:
-            raise CloudantException('You must provide a url or an account.')
+            raise CloudantClientException(102)
 
         if kwargs.get('connect', False):
             self.connect()
@@ -537,7 +533,7 @@ class Cloudant(CouchDB):
             between 1 and 12. Optional parameter. Defaults to None.
             If used, it must be accompanied by ``year``.
         """
-        err = None
+        err = False
         if year is None and month is None:
             resp = self.r_session.get(endpoint)
         else:
@@ -548,14 +544,14 @@ class Cloudant(CouchDB):
                             endpoint, str(int(year)), str(int(month)))
                     )
                 else:
-                    err = ('Invalid year and/or month supplied.  '
-                           'Found: year - {0}, month - {1}').format(year, month)
+                    err = True
             except (ValueError, TypeError):
-                err = ('Invalid year and/or month supplied.  '
-                       'Found: year - {0}, month - {1}').format(year, month)
+                err = True
 
         if err:
-            raise CloudantArgumentError(err)
+            msg = ('Invalid year and/or month supplied.  '
+                   'Found: year - {0}, month - {1}').format(year, month)
+            raise CloudantArgumentError(msg)
         else:
             resp.raise_for_status()
             return resp.json()
diff --git a/src/cloudant/database.py b/src/cloudant/database.py
index bf0a25c..14a92f2 100644
--- a/src/cloudant/database.py
+++ b/src/cloudant/database.py
@@ -34,7 +34,7 @@ from .security_document import SecurityDocument
 from .view import View
 from .index import Index, TextIndex, SpecialIndex
 from .query import Query
-from .error import CloudantException, CloudantArgumentError
+from .error import CloudantArgumentError, CloudantDatabaseException
 from .result import Result, QueryResult
 from .feed import Feed, InfiniteFeed
 
@@ -158,9 +158,7 @@ class CouchDatabase(dict):
         else:
             doc = Document(self, docid)
         if throw_on_exists and doc.exists():
-            raise CloudantException(
-                'Error - Document with id {0} already exists.'.format(docid)
-            )
+            raise CloudantDatabaseException(409, docid)
         doc.update(data)
         doc.create()
         super(CouchDatabase, self).__setitem__(doc['_id'], doc)
@@ -346,11 +344,8 @@ class CouchDatabase(dict):
         if resp.status_code == 201 or resp.status_code == 202:
             return self
 
-        raise CloudantException(
-            "Unable to create database {0}: Reason: {1}".format(
-                self.database_url, resp.text
-            ),
-            code=resp.status_code
+        raise CloudantDatabaseException(
+            resp.status_code, self.database_url, resp.text
         )
 
     def delete(self):
@@ -747,10 +742,7 @@ class CouchDatabase(dict):
         try:
             ret = int(resp.text)
         except ValueError:
-            resp.status_code = 400
-            raise CloudantException(
-                'Error - Invalid Response Value: {}'.format(resp.json())
-            )
+            raise CloudantDatabaseException(400, resp.json())
 
         return ret
 
@@ -1090,7 +1082,7 @@ class CloudantDatabase(CouchDatabase):
                     **data.get('def', {})
                 ))
             else:
-                raise CloudantException('Unexpected index content: {0} found.')
+                raise CloudantDatabaseException(101, data.get('type'))
         return indexes
 
     def create_query_index(
diff --git a/src/cloudant/design_document.py b/src/cloudant/design_document.py
index 74d430d..cdca554 100644
--- a/src/cloudant/design_document.py
+++ b/src/cloudant/design_document.py
@@ -19,7 +19,7 @@ from ._2to3 import iteritems_, STRTYPE
 from ._common_util import QUERY_LANGUAGE, codify
 from .document import Document
 from .view import View, QueryIndexView
-from .error import CloudantArgumentError, CloudantException
+from .error import CloudantArgumentError, CloudantDesignDocumentException
 
 class DesignDocument(Document):
     """
@@ -285,9 +285,7 @@ class DesignDocument(Document):
             msg = "View {0} already exists in this design doc".format(view_name)
             raise CloudantArgumentError(msg)
         if self.get('language', None) == QUERY_LANGUAGE:
-            msg = ('Cannot add a MapReduce view to a '
-                   'design document for query indexes.')
-            raise CloudantException(msg)
+            raise CloudantDesignDocumentException(101)
 
         view = View(self, view_name, map_func, reduce_func, **kwargs)
         self.views.__setitem__(view_name, view)
@@ -361,8 +359,7 @@ class DesignDocument(Document):
             msg = "View {0} does not exist in this design doc".format(view_name)
             raise CloudantArgumentError(msg)
         if isinstance(view, QueryIndexView):
-            msg = 'Cannot update a query index view using this method.'
-            raise CloudantException(msg)
+            raise CloudantDesignDocumentException(102)
 
         view = View(self, view_name, map_func, reduce_func, **kwargs)
         self.views.__setitem__(view_name, view)
@@ -432,8 +429,7 @@ class DesignDocument(Document):
         if view is None:
             return
         if isinstance(view, QueryIndexView):
-            msg = 'Cannot delete a query index view using this method.'
-            raise CloudantException(msg)
+            raise CloudantDesignDocumentException(103)
 
         self.views.__delitem__(view_name)
 
@@ -517,34 +513,22 @@ class DesignDocument(Document):
             if self.get('language', None) != QUERY_LANGUAGE:
                 for view_name, view in self.iterviews():
                     if isinstance(view, QueryIndexView):
-                        msg = 'View {0} must be of type View.'.format(view_name)
-                        raise CloudantException(msg)
+                        raise CloudantDesignDocumentException(104, view_name)
             else:
                 for view_name, view in self.iterviews():
                     if not isinstance(view, QueryIndexView):
-                        msg = (
-                            'View {0} must be of type QueryIndexView.'
-                        ).format(view_name)
-                        raise CloudantException(msg)
+                        raise CloudantDesignDocumentException(105, view_name)
 
         if self.indexes:
             if self.get('language', None) != QUERY_LANGUAGE:
                 for index_name, search in self.iterindexes():
                     # Check the instance of the javascript search function
                     if not isinstance(search['index'], STRTYPE):
-                        msg = (
-                            'Function for search index {0} must '
-                            'be of type string.'
-                        ).format(index_name)
-                        raise CloudantException(msg)
+                        raise CloudantDesignDocumentException(106, index_name)
             else:
                 for index_name, index in self.iterindexes():
                     if not isinstance(index['index'], dict):
-                        msg = (
-                            'Definition for query text index {0} must '
-                            'be of type dict.'
-                        ).format(index_name)
-                        raise CloudantException(msg)
+                        raise CloudantDesignDocumentException(107, index_name)
 
         for prop in self._nested_object_names:
             if not getattr(self, prop):
diff --git a/src/cloudant/document.py b/src/cloudant/document.py
index 0492777..500acc1 100644
--- a/src/cloudant/document.py
+++ b/src/cloudant/document.py
@@ -21,7 +21,8 @@ import requests
 from requests.exceptions import HTTPError
 
 from ._2to3 import url_quote, url_quote_plus
-from .error import CloudantException
+from .error import CloudantDocumentException
+
 
 class Document(dict):
     """
@@ -161,10 +162,7 @@ class Document(dict):
         the locally cached Document object.
         """
         if self.document_url is None:
-            raise CloudantException(
-                'A document id is required to fetch document contents.  '
-                'Add an _id key and value to the document and re-try.'
-            )
+            raise CloudantDocumentException(101)
         resp = self.r_session.get(self.document_url)
         resp.raise_for_status()
         self.clear()
@@ -210,9 +208,7 @@ class Document(dict):
         if doc.get(field) is None:
             doc[field] = []
         if not isinstance(doc[field], list):
-            raise CloudantException(
-                'The field {0} is not a list.'.format(field)
-            )
+            raise CloudantDocumentException(102, field)
         if value is not None:
             doc[field].append(value)
 
@@ -227,9 +223,7 @@ class Document(dict):
         :param value: Value to remove from the field list.
         """
         if not isinstance(doc[field], list):
-            raise CloudantException(
-                'The field {0} is not a list.'.format(field)
-            )
+            raise CloudantDocumentException(102, field)
         doc[field].remove(value)
 
     @staticmethod
@@ -314,10 +308,7 @@ class Document(dict):
         object.
         """
         if not self.get("_rev"):
-            raise CloudantException(
-                "Attempting to delete a doc with no _rev. Try running "
-                ".fetch first!"
-            )
+            raise CloudantDocumentException(103)
 
         del_resp = self.r_session.delete(
             self.document_url,
diff --git a/src/cloudant/error.py b/src/cloudant/error.py
index 4769062..a09743c 100644
--- a/src/cloudant/error.py
+++ b/src/cloudant/error.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (c) 2015 IBM. All rights reserved.
+# Copyright (c) 2015, 2016 IBM. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -16,6 +16,16 @@
 Module that contains common exception classes for the Cloudant Python client
 library.
 """
+from cloudant._messages import (
+    CLIENT,
+    DATABASE,
+    DESIGN_DOCUMENT,
+    DOCUMENT,
+    FEED,
+    INDEX,
+    REPLICATOR,
+    RESULT
+)
 
 class CloudantException(Exception):
     """
@@ -61,18 +71,115 @@ class ResultException(CloudantException):
     """
 
     def __init__(self, code=100, *args):
-        messages = {
-            100:'A general result exception was raised.',
-            101:'Failed to interpret the argument {0} as a valid key value or as a valid slice.',
-            102:'Cannot use {0} when performing key access or key slicing.  Found {1}.',
-            103:'Cannot use {0} for iteration.  Found {1}.',
-            104:'Invalid page_size: {0}'
-        }
-
         try:
-            msg = messages[code].format(*args)
-        # pylint: disable=broad-except
-        except Exception:
+            msg = RESULT[code].format(*args)
+        except (KeyError, IndexError):
             code = 100
-            msg = messages[code]
+            msg = RESULT[code]
         super(ResultException, self).__init__(msg, code)
+
+
+class CloudantClientException(CloudantException):
+    """
+    Provides a way to issue Cloudant library client specific exceptions.
+
+    :param int code: A code value used to identify the client exception.
+    :param args: A list of arguments used to format the exception message.
+    """
+    def __init__(self, code=100, *args):
+        try:
+            msg = CLIENT[code].format(*args)
+        except (KeyError, IndexError):
+            code = 100
+            msg = CLIENT[code]
+        super(CloudantClientException, self).__init__(msg, code)
+
+class CloudantDatabaseException(CloudantException):
+    """
+    Provides a way to issue Cloudant library database specific exceptions.
+
+    :param int code: A code value used to identify the database exception.
+    :param args: A list of arguments used to format the exception message.
+    """
+    def __init__(self, code=100, *args):
+        try:
+            msg = DATABASE[code].format(*args)
+        except (KeyError, IndexError):
+            code = 100
+            msg = DATABASE[code]
+        super(CloudantDatabaseException, self).__init__(msg, code)
+
+class CloudantDesignDocumentException(CloudantException):
+    """
+    Provides a way to issue Cloudant library design document exceptions.
+
+    :param int code: A code value used to identify the design doc exception.
+    :param args: A list of arguments used to format the exception message.
+    """
+    def __init__(self, code=100, *args):
+        try:
+            msg = DESIGN_DOCUMENT[code].format(*args)
+        except (KeyError, IndexError):
+            code = 100
+            msg = DESIGN_DOCUMENT[code]
+        super(CloudantDesignDocumentException, self).__init__(msg, code)
+
+class CloudantDocumentException(CloudantException):
+    """
+    Provides a way to issue Cloudant library document specific exceptions.
+
+    :param int code: A code value used to identify the document exception.
+    :param args: A list of arguments used to format the exception message.
+    """
+    def __init__(self, code=100, *args):
+        try:
+            msg = DOCUMENT[code].format(*args)
+        except (KeyError, IndexError):
+            code = 100
+            msg = DOCUMENT[code]
+        super(CloudantDocumentException, self).__init__(msg, code)
+
+class CloudantFeedException(CloudantException):
+    """
+    Provides a way to issue Cloudant library feed specific exceptions.
+
+    :param int code: A code value used to identify the feed exception.
+    :param args: A list of arguments used to format the exception message.
+    """
+    def __init__(self, code=100, *args):
+        try:
+            msg = FEED[code].format(*args)
+        except (KeyError, IndexError):
+            code = 100
+            msg = FEED[code]
+        super(CloudantFeedException, self).__init__(msg, code)
+
+class CloudantIndexException(CloudantException):
+    """
+    Provides a way to issue Cloudant library index specific exceptions.
+
+    :param int code: A code value used to identify the index exception.
+    :param args: A list of arguments used to format the exception message.
+    """
+    def __init__(self, code=100, *args):
+        try:
+            msg = INDEX[code].format(*args)
+        except (KeyError, IndexError):
+            code = 100
+            msg = INDEX[code]
+        super(CloudantIndexException, self).__init__(msg, code)
+
+class CloudantReplicatorException(CloudantException):
+    """
+    Provides a way to issue Cloudant library replicator specific exceptions.
+
+    :param int code: A code value used to identify the replicator exception.
+    :param args: A list of arguments used to format the exception message.
+    """
+    def __init__(self, code=100, *args):
+        try:
+            msg = REPLICATOR[code].format(*args)
+        except (KeyError, IndexError):
+            code = 100
+            msg = REPLICATOR[code]
+        super(CloudantReplicatorException, self).__init__(msg, code)
diff --git a/src/cloudant/feed.py b/src/cloudant/feed.py
index 32b833a..a5b85fc 100644
--- a/src/cloudant/feed.py
+++ b/src/cloudant/feed.py
@@ -20,7 +20,7 @@ continuous and non-continuous feeds like ``_changes`` and ``_db_updates``.
 import json
 
 from ._2to3 import iteritems_, next_, unicode_, STRTYPE, NONETYPE
-from .error import CloudantArgumentError, CloudantException
+from .error import CloudantArgumentError, CloudantFeedException
 from ._common_util import feed_arg_types, TYPE_CONVERTERS
 
 class Feed(object):
@@ -252,8 +252,7 @@ class InfiniteFeed(Feed):
         """
         while True:
             if self._source == 'CouchDB':
-                raise CloudantException(
-                    'Infinite _db_updates feed not supported for CouchDB.')
+                raise CloudantFeedException(101)
             if self._last_seq:
                 self._options.update({'since': self._last_seq})
                 self._resp = None
diff --git a/src/cloudant/index.py b/src/cloudant/index.py
index 87b2fec..3ec3dad 100644
--- a/src/cloudant/index.py
+++ b/src/cloudant/index.py
@@ -24,7 +24,7 @@ from ._common_util import JSON_INDEX_TYPE
 from ._common_util import TEXT_INDEX_TYPE
 from ._common_util import SPECIAL_INDEX_TYPE
 from ._common_util import TEXT_INDEX_ARGS
-from .error import CloudantArgumentError, CloudantException
+from .error import CloudantArgumentError, CloudantIndexException
 
 class Index(object):
     """
@@ -252,13 +252,11 @@ class SpecialIndex(Index):
         A "special" index cannot be created.  This method is disabled for a
         SpecialIndex object.
         """
-        msg = 'Creating the \"special\" index is not allowed.'
-        raise CloudantException(msg)
+        raise CloudantIndexException(101)
 
     def delete(self):
         """
         A "special" index cannot be deleted.  This method is disabled for a
         SpecialIndex object.
         """
-        msg = 'Deleting the \"special\" index is not allowed.'
-        raise CloudantException(msg)
+        raise CloudantIndexException(102)
diff --git a/src/cloudant/replicator.py b/src/cloudant/replicator.py
index c858ec2..06e91b0 100644
--- a/src/cloudant/replicator.py
+++ b/src/cloudant/replicator.py
@@ -18,7 +18,7 @@ API module/class for handling database replications
 
 import uuid
 
-from .error import CloudantException
+from .error import CloudantReplicatorException, CloudantClientException
 from .document import Document
 
 class Replicator(object):
@@ -33,13 +33,11 @@ class Replicator(object):
     """
 
     def __init__(self, client):
+        repl_db = '_replicator'
         try:
-            self.database = client['_replicator']
+            self.database = client[repl_db]
         except Exception:
-            raise CloudantException(
-                'Unable to acquire _replicator database.  '
-                'Verify that the client is valid and try again.'
-            )
+            raise CloudantClientException(404, repl_db)
 
     def create_replication(self, source_db=None, target_db=None,
                            repl_id=None, **kwargs):
@@ -78,10 +76,7 @@ class Replicator(object):
 
         if not data.get('source'):
             if source_db is None:
-                raise CloudantException(
-                    'You must specify either a source_db Database '
-                    'object or a manually composed \'source\' string/dict.'
-                )
+                raise CloudantReplicatorException(101)
             data['source'] = {'url': source_db.database_url}
             if not source_db.admin_party:
                 data['source'].update(
@@ -90,10 +85,7 @@ class Replicator(object):
 
         if not data.get('target'):
             if target_db is None:
-                raise CloudantException(
-                    'You must specify either a target_db Database '
-                    'object or a manually composed \'target\' string/dict.'
-                )
+                raise CloudantReplicatorException(102)
             data['target'] = {'url': target_db.database_url}
             if not target_db.admin_party:
                 data['target'].update(
@@ -136,9 +128,7 @@ class Replicator(object):
         try:
             repl_doc = self.database[repl_id]
         except KeyError:
-            raise CloudantException(
-                "Replication {} not found".format(repl_id)
-            )
+            raise CloudantReplicatorException(404, repl_id)
         repl_doc.fetch()
         return repl_doc.get('_replication_state')
 
@@ -203,8 +193,7 @@ class Replicator(object):
         try:
             repl_doc = self.database[repl_id]
         except KeyError:
-            raise CloudantException(
-                "Could not find replication with id {}".format(repl_id))
+            raise CloudantReplicatorException(404, repl_id)
 
         repl_doc.fetch()
         repl_doc.delete()


Test patch:
diff --git a/tests/unit/client_tests.py b/tests/unit/client_tests.py
index 49a60ac..8ec4286 100644
--- a/tests/unit/client_tests.py
+++ b/tests/unit/client_tests.py
@@ -30,13 +30,51 @@ import datetime
 
 from cloudant import cloudant, couchdb, couchdb_admin_party
 from cloudant.client import Cloudant, CouchDB
-from cloudant.error import CloudantException, CloudantArgumentError
+from cloudant.error import CloudantArgumentError, CloudantClientException
 from cloudant.feed import Feed, InfiniteFeed
 from cloudant._common_util import InfiniteSession
 
 from .unit_t_db_base import UnitTestDbBase
 from .. import bytes_, str_
 
+class CloudantClientExceptionTests(unittest.TestCase):
+    """
+    Ensure CloudantClientException functions as expected.
+    """
+
+    def test_raise_without_code(self):
+        """
+        Ensure that a default exception/code is used if none is provided.
+        """
+        with self.assertRaises(CloudantClientException) as cm:
+            raise CloudantClientException()
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_using_invalid_code(self):
+        """
+        Ensure that a default exception/code is used if invalid code is provided.
+        """
+        with self.assertRaises(CloudantClientException) as cm:
+            raise CloudantClientException('foo')
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_without_args(self):
+        """
+        Ensure that a default exception/code is used if the message requested
+        by the code provided requires an argument list and none is provided.
+        """
+        with self.assertRaises(CloudantClientException) as cm:
+            raise CloudantClientException(404)
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_with_proper_code_and_args(self):
+        """
+        Ensure that the requested exception is raised.
+        """
+        with self.assertRaises(CloudantClientException) as cm:
+            raise CloudantClientException(404, 'foo')
+        self.assertEqual(cm.exception.status_code, 404)
+
 class ClientTests(UnitTestDbBase):
     """
     CouchDB/Cloudant client unit tests
@@ -233,19 +271,14 @@ class ClientTests(UnitTestDbBase):
         Test creation of already existing database
         """
         dbname = self.dbname()
-        try:
-            self.client.connect()
-            self.client.create_database(dbname)
+        self.client.connect()
+        self.client.create_database(dbname)
+        with self.assertRaises(CloudantClientException) as cm:
             self.client.create_database(dbname, throw_on_exists=True)
-            self.fail('Above statement should raise a CloudantException')
-        except CloudantException as err:
-            self.assertEqual(
-                str(err),
-                'Database {0} already exists'.format(dbname)
-                )
-        finally:
-            self.client.delete_database(dbname)
-            self.client.disconnect()
+        self.assertEqual(cm.exception.status_code, 409)
+
+        self.client.delete_database(dbname)
+        self.client.disconnect()
 
     def test_delete_non_existing_database(self):
         """
@@ -255,8 +288,9 @@ class ClientTests(UnitTestDbBase):
             self.client.connect()
             self.client.delete_database('no_such_db')
             self.fail('Above statement should raise a CloudantException')
-        except CloudantException as err:
-            self.assertEqual(str(err), 'Database no_such_db does not exist')
+        except CloudantClientException as err:
+            self.assertEqual(str(err), 'Database no_such_db does not exist. '
+                                       'Verify that the client is valid and try again.')
         finally:
             self.client.disconnect()
 
@@ -396,8 +430,10 @@ class ClientTests(UnitTestDbBase):
             self.client.connect()
             self.client['not-a-db'] = 'This is not a database object'
             self.fail('Above statement should raise a CloudantException')
-        except CloudantException as err:
-            self.assertEqual(str(err), 'Value must be set to a Database object')
+        except CloudantClientException as err:
+            self.assertEqual(
+                str(err),
+                'Value must be set to a Database object. Found type: str.')
         finally:
             self.client.disconnect()
 
diff --git a/tests/unit/database_tests.py b/tests/unit/database_tests.py
index afc6734..a6c0f4e 100644
--- a/tests/unit/database_tests.py
+++ b/tests/unit/database_tests.py
@@ -30,7 +30,7 @@ import os
 import uuid
 
 from cloudant.result import Result, QueryResult
-from cloudant.error import CloudantException, CloudantArgumentError
+from cloudant.error import CloudantArgumentError, CloudantDatabaseException
 from cloudant.document import Document
 from cloudant.design_document import DesignDocument
 from cloudant.security_document import SecurityDocument
@@ -40,6 +40,44 @@ from cloudant.feed import Feed, InfiniteFeed
 from .unit_t_db_base import UnitTestDbBase
 from .. import unicode_
 
+class CloudantDatabaseExceptionTests(unittest.TestCase):
+    """
+    Ensure CloudantDatabaseException functions as expected.
+    """
+
+    def test_raise_without_code(self):
+        """
+        Ensure that a default exception/code is used if none is provided.
+        """
+        with self.assertRaises(CloudantDatabaseException) as cm:
+            raise CloudantDatabaseException()
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_using_invalid_code(self):
+        """
+        Ensure that a default exception/code is used if invalid code is provided.
+        """
+        with self.assertRaises(CloudantDatabaseException) as cm:
+            raise CloudantDatabaseException('foo')
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_without_args(self):
+        """
+        Ensure that a default exception/code is used if the message requested
+        by the code provided requires an argument list and none is provided.
+        """
+        with self.assertRaises(CloudantDatabaseException) as cm:
+            raise CloudantDatabaseException(400)
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_with_proper_code_and_args(self):
+        """
+        Ensure that the requested exception is raised.
+        """
+        with self.assertRaises(CloudantDatabaseException) as cm:
+            raise CloudantDatabaseException(400, 'foo')
+        self.assertEqual(cm.exception.status_code, 400)
+
 class DatabaseTests(UnitTestDbBase):
     """
     CouchDatabase/CloudantDatabase unit tests
@@ -217,10 +255,10 @@ class DatabaseTests(UnitTestDbBase):
         try:
             self.db.create_document(data, throw_on_exists=True)
             self.fail('Above statement should raise a CloudantException')
-        except CloudantException as err:
+        except CloudantDatabaseException as err:
             self.assertEqual(
                 str(err),
-                'Error - Document with id julia06 already exists.'
+                'Document with id julia06 already exists.'
                 )
 
     def test_create_document_without_id(self):
diff --git a/tests/unit/design_document_tests.py b/tests/unit/design_document_tests.py
index c0f25d9..196a11c 100644
--- a/tests/unit/design_document_tests.py
+++ b/tests/unit/design_document_tests.py
@@ -30,10 +30,48 @@ import requests
 from cloudant.document import Document 
 from cloudant.design_document import DesignDocument
 from cloudant.view import View, QueryIndexView
-from cloudant.error import CloudantArgumentError, CloudantException
+from cloudant.error import CloudantArgumentError, CloudantDesignDocumentException
 
 from .unit_t_db_base import UnitTestDbBase
 
+class CloudantDesignDocumentExceptionTests(unittest.TestCase):
+    """
+    Ensure CloudantDesignDocumentException functions as expected.
+    """
+
+    def test_raise_without_code(self):
+        """
+        Ensure that a default exception/code is used if none is provided.
+        """
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
+            raise CloudantDesignDocumentException()
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_using_invalid_code(self):
+        """
+        Ensure that a default exception/code is used if invalid code is provided.
+        """
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
+            raise CloudantDesignDocumentException('foo')
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_without_args(self):
+        """
+        Ensure that a default exception/code is used if the message requested
+        by the code provided requires an argument list and none is provided.
+        """
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
+            raise CloudantDesignDocumentException(104)
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_with_proper_code_and_args(self):
+        """
+        Ensure that the requested exception is raised.
+        """
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
+            raise CloudantDesignDocumentException(104, 'foo')
+        self.assertEqual(cm.exception.status_code, 104)
+
 class DesignDocumentTests(UnitTestDbBase):
     """
     DesignDocument unit tests
@@ -173,7 +211,7 @@ class DesignDocumentTests(UnitTestDbBase):
         """
         ddoc = DesignDocument(self.db, '_design/ddoc001')
         ddoc['language'] = 'query'
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
             ddoc.add_view('view001', {'foo': 'bar'})
         err = cm.exception
         self.assertEqual(
@@ -239,7 +277,7 @@ class DesignDocumentTests(UnitTestDbBase):
         self.db.create_document(data)
         ddoc = DesignDocument(self.db, '_design/ddoc001')
         ddoc.fetch()
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
             ddoc.update_view(
                 'view001',
                 'function (doc) {\n  emit(doc._id, 1);\n}'
@@ -283,7 +321,7 @@ class DesignDocumentTests(UnitTestDbBase):
         self.db.create_document(data)
         ddoc = DesignDocument(self.db, '_design/ddoc001')
         ddoc.fetch()
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
             ddoc.delete_view('view001')
         err = cm.exception
         self.assertEqual(
@@ -504,7 +542,7 @@ class DesignDocumentTests(UnitTestDbBase):
             'analyzer': {'name': 'perfield','default': 'keyword',
                          'fields': {'$default': 'german'}}}
         self.assertIsInstance(ddoc['indexes']['index001']['index'], dict)
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
             ddoc.save()
         err = cm.exception
         self.assertEqual(
@@ -539,7 +577,7 @@ class DesignDocumentTests(UnitTestDbBase):
             'analyzer': {'name': 'perfield','default': 'keyword',
                          'fields': {'$default': 'german'}}}
         self.assertIsInstance(ddoc_remote['indexes']['index001']['index'], dict)
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
             ddoc_remote.save()
         err = cm.exception
         self.assertEqual(
@@ -558,7 +596,7 @@ class DesignDocumentTests(UnitTestDbBase):
         db_copy = '{0}-copy'.format(self.db.database_name)
         ddoc.add_view('view001', view_map, view_reduce)
         ddoc['language'] = 'query'
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
             ddoc.save()
         err = cm.exception
         self.assertEqual(
@@ -600,13 +638,13 @@ class DesignDocumentTests(UnitTestDbBase):
         self.db.create_document(data)
         ddoc = DesignDocument(self.db, '_design/ddoc001')
         ddoc.fetch()
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
             ddoc['language'] = 'not-query'
             ddoc.save()
         err = cm.exception
         self.assertEqual(str(err), 'View view001 must be of type View.')
 
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
             del ddoc['language']
             ddoc.save()
         err = cm.exception
@@ -1043,7 +1081,7 @@ class DesignDocumentTests(UnitTestDbBase):
                      '{"store": true}); }\n}',
             'analyzer': 'standard'}
         self.assertIsInstance(ddoc['indexes']['search001']['index'], str)
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
             ddoc.save()
         err = cm.exception
         self.assertEqual(
@@ -1076,7 +1114,7 @@ class DesignDocumentTests(UnitTestDbBase):
         self.assertIsInstance(
             ddoc['indexes']['search001']['index'], str
         )
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantDesignDocumentException) as cm:
             ddoc.save()
         err = cm.exception
         self.assertEqual(
diff --git a/tests/unit/document_tests.py b/tests/unit/document_tests.py
index 4aaa57f..2d76389 100644
--- a/tests/unit/document_tests.py
+++ b/tests/unit/document_tests.py
@@ -32,7 +32,7 @@ import uuid
 import inspect
 
 from cloudant.document import Document
-from cloudant.error import CloudantException
+from cloudant.error import CloudantDocumentException
 
 from .. import StringIO, unicode_
 from .unit_t_db_base import UnitTestDbBase
@@ -43,6 +43,44 @@ def find_fixture(name):
     filename = os.path.join(dirname, name)
     return filename
 
+class CloudantDocumentExceptionTests(unittest.TestCase):
+    """
+    Ensure CloudantDocumentException functions as expected.
+    """
+
+    def test_raise_without_code(self):
+        """
+        Ensure that a default exception/code is used if none is provided.
+        """
+        with self.assertRaises(CloudantDocumentException) as cm:
+            raise CloudantDocumentException()
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_using_invalid_code(self):
+        """
+        Ensure that a default exception/code is used if invalid code is provided.
+        """
+        with self.assertRaises(CloudantDocumentException) as cm:
+            raise CloudantDocumentException('foo')
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_without_args(self):
+        """
+        Ensure that a default exception/code is used if the message requested
+        by the code provided requires an argument list and none is provided.
+        """
+        with self.assertRaises(CloudantDocumentException) as cm:
+            raise CloudantDocumentException(102)
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_with_proper_code_and_args(self):
+        """
+        Ensure that the requested exception is raised.
+        """
+        with self.assertRaises(CloudantDocumentException) as cm:
+            raise CloudantDocumentException(102, 'foo')
+        self.assertEqual(cm.exception.status_code, 102)
+
 class DocumentTests(UnitTestDbBase):
     """
     Document unit tests
@@ -230,10 +268,10 @@ class DocumentTests(UnitTestDbBase):
         try:
             doc.fetch()
             self.fail('Above statement should raise an Exception')
-        except CloudantException as err:
+        except CloudantDocumentException as err:
             self.assertEqual(
                 str(err),
-                'A document id is required to fetch document contents.  '
+                'A document id is required to fetch document contents. '
                 'Add an _id key and value to the document and re-try.'
             )
 
@@ -382,7 +420,7 @@ class DocumentTests(UnitTestDbBase):
         try:
             doc.list_field_append(doc, 'name', 'isabel')
             self.fail('Above statement should raise an Exception')
-        except CloudantException as err:
+        except CloudantDocumentException as err:
             self.assertEqual(str(err), 'The field name is not a list.')
         self.assertEqual(doc, {'name': 'julia'})
 
@@ -408,7 +446,7 @@ class DocumentTests(UnitTestDbBase):
         try:
             doc.list_field_remove(doc, 'name', 'julia')
             self.fail('Above statement should raise an Exception')
-        except CloudantException as err:
+        except CloudantDocumentException as err:
             self.assertEqual(str(err), 'The field name is not a list.')
         self.assertEqual(doc, {'name': 'julia'})
 
@@ -455,11 +493,11 @@ class DocumentTests(UnitTestDbBase):
         try:
             doc.delete()
             self.fail('Above statement should raise an Exception')
-        except CloudantException as err:
+        except CloudantDocumentException as err:
             self.assertEqual(
                 str(err), 
                 'Attempting to delete a doc with no _rev. '
-                'Try running .fetch first!'
+                'Try running .fetch and re-try.'
             )
 
     def test_delete_document_success(self):
diff --git a/tests/unit/index_tests.py b/tests/unit/index_tests.py
index 6f316f4..398e308 100644
--- a/tests/unit/index_tests.py
+++ b/tests/unit/index_tests.py
@@ -33,11 +33,40 @@ from cloudant.query import Query
 from cloudant.view import QueryIndexView
 from cloudant.design_document import DesignDocument
 from cloudant.document import Document
-from cloudant.error import CloudantArgumentError, CloudantException
+from cloudant.error import CloudantArgumentError, CloudantIndexException
 
 from .. import PY2
 from .unit_t_db_base import UnitTestDbBase
 
+class CloudantIndexExceptionTests(unittest.TestCase):
+    """
+    Ensure CloudantIndexException functions as expected.
+    """
+
+    def test_raise_without_code(self):
+        """
+        Ensure that a default exception/code is used if none is provided.
+        """
+        with self.assertRaises(CloudantIndexException) as cm:
+            raise CloudantIndexException()
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_using_invalid_code(self):
+        """
+        Ensure that a default exception/code is used if invalid code is provided.
+        """
+        with self.assertRaises(CloudantIndexException) as cm:
+            raise CloudantIndexException('foo')
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_with_proper_code_and_args(self):
+        """
+        Ensure that the requested exception is raised.
+        """
+        with self.assertRaises(CloudantIndexException) as cm:
+            raise CloudantIndexException(101)
+        self.assertEqual(cm.exception.status_code, 101)
+
 @unittest.skipUnless(
     os.environ.get('RUN_CLOUDANT_TESTS') is not None,
     'Skipping Cloudant Index tests'
@@ -591,7 +620,7 @@ class SpecialIndexTests(unittest.TestCase):
         Test that the SpecialIndex create method is disabled.
         """
         index = SpecialIndex(self.db, fields=[{'_id': 'asc'}])
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantIndexException) as cm:
             index.create()
         err = cm.exception
         self.assertEqual(
@@ -604,7 +633,7 @@ class SpecialIndexTests(unittest.TestCase):
         Test that the SpecialIndex delete method is disabled.
         """
         index = SpecialIndex(self.db, fields=[{'_id': 'asc'}])
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantIndexException) as cm:
             index.delete()
         err = cm.exception
         self.assertEqual(
diff --git a/tests/unit/infinite_feed_tests.py b/tests/unit/infinite_feed_tests.py
index 5092f9a..f7b2ee4 100644
--- a/tests/unit/infinite_feed_tests.py
+++ b/tests/unit/infinite_feed_tests.py
@@ -24,7 +24,7 @@ from time import sleep
 
 from cloudant.feed import InfiniteFeed, Feed
 from cloudant.client import CouchDB
-from cloudant.error import CloudantException, CloudantArgumentError
+from cloudant.error import CloudantArgumentError, CloudantFeedException
 
 from .unit_t_db_base import UnitTestDbBase
 
@@ -42,6 +42,35 @@ class MethodCallCount(object):
         self.called_count += 1
         self._ref()
 
+class CloudantFeedExceptionTests(unittest.TestCase):
+    """
+    Ensure CloudantFeedException functions as expected.
+    """
+
+    def test_raise_without_code(self):
+        """
+        Ensure that a default exception/code is used if none is provided.
+        """
+        with self.assertRaises(CloudantFeedException) as cm:
+            raise CloudantFeedException()
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_using_invalid_code(self):
+        """
+        Ensure that a default exception/code is used if invalid code is provided.
+        """
+        with self.assertRaises(CloudantFeedException) as cm:
+            raise CloudantFeedException('foo')
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_with_proper_code_and_args(self):
+        """
+        Ensure that the requested exception is raised.
+        """
+        with self.assertRaises(CloudantFeedException) as cm:
+            raise CloudantFeedException(101)
+        self.assertEqual(cm.exception.status_code, 101)
+
 class InfiniteFeedTests(UnitTestDbBase):
     """
     Infinite Feed unit tests
@@ -101,7 +130,7 @@ class InfiniteFeedTests(UnitTestDbBase):
         """
         Ensure that a CouchDB client cannot be used with an infinite feed.
         """
-        with self.assertRaises(CloudantException) as cm:
+        with self.assertRaises(CloudantFeedException) as cm:
             invalid_feed = [x for x in InfiniteFeed(self.client)]
         self.assertEqual(str(cm.exception),
             'Infinite _db_updates feed not supported for CouchDB.')
diff --git a/tests/unit/replicator_tests.py b/tests/unit/replicator_tests.py
index 2cbcb4c..d5f041d 100644
--- a/tests/unit/replicator_tests.py
+++ b/tests/unit/replicator_tests.py
@@ -29,11 +29,49 @@ import requests
 
 from cloudant.replicator import Replicator
 from cloudant.document import Document
-from cloudant.error import CloudantException
+from cloudant.error import CloudantReplicatorException, CloudantClientException
 
 from .unit_t_db_base import UnitTestDbBase
 from .. import unicode_
 
+class CloudantReplicatorExceptionTests(unittest.TestCase):
+    """
+    Ensure CloudantReplicatorException functions as expected.
+    """
+
+    def test_raise_without_code(self):
+        """
+        Ensure that a default exception/code is used if none is provided.
+        """
+        with self.assertRaises(CloudantReplicatorException) as cm:
+            raise CloudantReplicatorException()
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_using_invalid_code(self):
+        """
+        Ensure that a default exception/code is used if invalid code is provided.
+        """
+        with self.assertRaises(CloudantReplicatorException) as cm:
+            raise CloudantReplicatorException('foo')
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_without_args(self):
+        """
+        Ensure that a default exception/code is used if the message requested
+        by the code provided requires an argument list and none is provided.
+        """
+        with self.assertRaises(CloudantReplicatorException) as cm:
+            raise CloudantReplicatorException(404)
+        self.assertEqual(cm.exception.status_code, 100)
+
+    def test_raise_with_proper_code_and_args(self):
+        """
+        Ensure that the requested exception is raised.
+        """
+        with self.assertRaises(CloudantReplicatorException) as cm:
+            raise CloudantReplicatorException(404, 'foo')
+        self.assertEqual(cm.exception.status_code, 404)
+
 class ReplicatorTests(UnitTestDbBase):
     """
     Replicator unit tests
@@ -102,10 +140,10 @@ class ReplicatorTests(UnitTestDbBase):
             self.client.disconnect()
             repl = Replicator(self.client)
             self.fail('Above statement should raise a CloudantException')
-        except CloudantException as err:
+        except CloudantClientException as err:
             self.assertEqual(
                 str(err),
-                'Unable to acquire _replicator database.  '
+                'Database _replicator does not exist. '
                 'Verify that the client is valid and try again.'
             )
         finally:
@@ -177,7 +215,7 @@ class ReplicatorTests(UnitTestDbBase):
         try:
             repl_doc = self.replicator.create_replication()
             self.fail('Above statement should raise a CloudantException')
-        except CloudantException as err:
+        except CloudantReplicatorException as err:
             self.assertEqual(
                 str(err),
                 'You must specify either a source_db Database '
@@ -192,7 +230,7 @@ class ReplicatorTests(UnitTestDbBase):
         try:
             repl_doc = self.replicator.create_replication(self.db)
             self.fail('Above statement should raise a CloudantException')
-        except CloudantException as err:
+        except CloudantReplicatorException as err:
             self.assertEqual(
                 str(err),
                 'You must specify either a target_db Database '
@@ -252,10 +290,10 @@ class ReplicatorTests(UnitTestDbBase):
         try:
             self.replicator.replication_state(repl_id)
             self.fail('Above statement should raise a CloudantException')
-        except CloudantException as err:
+        except CloudantReplicatorException as err:
             self.assertEqual(
                 str(err),
-                'Replication {} not found'.format(repl_id)
+                'Replication with id {} not found.'.format(repl_id)
             )
             self.assertIsNone(repl_state)
 
@@ -288,10 +326,10 @@ class ReplicatorTests(UnitTestDbBase):
         try:
             self.replicator.stop_replication(repl_id)
             self.fail('Above statement should raise a CloudantException')
-        except CloudantException as err:
+        except CloudantReplicatorException as err:
             self.assertEqual(
                 str(err),
-                'Could not find replication with id {}'.format(repl_id)
+                'Replication with id {} not found.'.format(repl_id)
             )
 
     def test_follow_replication(self):