diff --git a/ops/model.py b/ops/model.py
index 88c0e29..0ee56ef 100644
--- a/ops/model.py
+++ b/ops/model.py
@@ -18,6 +18,7 @@ import datetime
 import decimal
 import ipaddress
 import json
+import logging
 import os
 import re
 import shutil
@@ -37,6 +38,17 @@ import ops
 import ops.pebble as pebble
 
 
+logger = logging.getLogger(__name__)
+
+ErrorsWithMessage = (
+    pebble.APIError,
+    pebble.ConnectionError,
+    pebble.PathError,
+    pebble.ProtocolError,
+    pebble.TimeoutError,
+)
+
+
 class Model:
     """Represents the Juju Model as seen from this unit.
 
@@ -1040,12 +1052,56 @@ class Container:
             socket_path = '/charm/containers/{}/pebble.socket'.format(name)
             pebble_client = backend.get_pebble(socket_path)
         self._pebble = pebble_client
+        self._completed = None
 
     @property
     def pebble(self) -> 'pebble.Client':
         """The low-level :class:`ops.pebble.Client` instance for this container."""
         return self._pebble
 
+    @property
+    def completed(self) -> bool:
+        """Whether or not a :meth:`is_ready` context finished successfully."""
+        return self._completed
+
+    def is_ready(self) -> '_ContainerReady':
+        """Check whether or not Pebble is ready as a simple property.
+
+        :meth:`is_ready` returns a :class:_ContainerReady `contextmanager` which
+         can be used in charms to wrap :class:`Container` operations which depend
+         on the Pebble backend being available. When `is_ready` is used, exceptions
+         from the underlying Pebble operations will log error messages rather than
+         raising exceptions.
+
+        Example:
+            ```
+            container = self.unit.get_container("example")
+            with container.is_ready() as c:
+                c.pull('/does/not/exist')
+
+                # This point of execution will not be reached if an exception
+                # was caught earlier
+                c.get_service("foo")
+            c.completed # False
+            ```
+
+            This will result in an `ERROR` log from PathError, but not a
+            traceback. In addition, the block running inside the contextmanager
+            will exit and return to the previous point of execution. Whether
+            or not the block completed successfully is available as a property
+
+        :meth:`is_ready` can also be used as a bare function, which will log an
+        error if the container is not ready.
+
+        Example:
+            ```
+            if container.is_ready():
+                do_something()
+            else:
+                do_something_else()
+        """
+        return _ContainerReady(self)
+
     def autostart(self):
         """Autostart all services marked as startup: enabled."""
         self._pebble.autostart_services()
@@ -1054,12 +1110,22 @@ class Container:
         """Start given service(s) by name."""
         if not service_names:
             raise TypeError('start expected at least 1 argument, got 0')
+
+        self._pebble.start_services(service_names)
+
+    def restart(self, *service_names: str):
+        """Restart the given service(s) by name."""
+        if not service_names:
+            raise TypeError('restart expected at least 1 argument, got 0')
+
+        self._pebble.stop_services(service_names)
         self._pebble.start_services(service_names)
 
     def stop(self, *service_names: str):
         """Stop given service(s) by name."""
         if not service_names:
             raise TypeError('stop expected at least 1 argument, got 0')
+
         self._pebble.stop_services(service_names)
 
     # TODO(benhoyt) - should be: layer: typing.Union[str, typing.Dict, 'pebble.Layer'],
@@ -1193,6 +1259,47 @@ class Container:
         self._pebble.remove_path(path, recursive=recursive)
 
 
+class _ContainerReady:
+    """Represents whether or not a container is ready as a Context Manager.
+
+    This class should not be instantiated directly, instead use :meth:`Container.is_ready`
+
+    Attributes:
+        container: A :class:`Container` object
+    """
+
+    def __init__(self, container: Container):
+        self.container = container
+
+    def __bool__(self) -> bool:
+        try:
+            # We don't care at all whether not the services are up in
+            # this case, just whether Pebble throws an error. If it doesn't,
+            # carry on with the contextmanager.
+            self.container._pebble.get_services()
+        except ErrorsWithMessage as e:
+            logger.error("Pebble is not ready! (%s) was raised due to: %s",
+                         e.name, e.message)
+            return False
+        return True
+
+    def __enter__(self) -> 'Container':
+        self.container._completed = True
+        return self.container
+
+    def __exit__(self, exc_type, e, exc_tb):
+        if exc_type in ErrorsWithMessage:
+            logger.error("(%s) was raised due to: %s", e.name, e.message)
+            self.container._completed = False
+            return True
+
+        if exc_type is pebble.ChangeError:
+            logger.error("Pebble could not apply the requested change (%s) "
+                         "due to %s", e.change, e.err)
+            self.container._completed = False
+            return True
+
+
 class ContainerMapping(Mapping):
     """Map of container names to Container objects.
 
@@ -1244,6 +1351,19 @@ class ModelError(Exception):
     pass
 
 
+class UnknownServiceError(Exception):
+    """Raised by :class:`Container` objects when Pebble cannot find a service.
+
+    This is done so authors can have a single catch-all exception if the service
+    cannot be found, typically due to asking for the service before
+    :meth:`Container.add_layer` has been called.
+    """
+
+
+class PebbleNotReadyError(Exception):
+    """Raised by :class:`Container` methods if the underlying Pebble socket returns an error."""
+
+
 class TooManyRelatedAppsError(ModelError):
     """Raised by :meth:`Model.get_relation` if there is more than one related application."""
 
diff --git a/ops/pebble.py b/ops/pebble.py
index c65e8c9..4155d61 100644
--- a/ops/pebble.py
+++ b/ops/pebble.py
@@ -126,6 +126,17 @@ def _json_loads(s: typing.Union[str, bytes]) -> typing.Dict:
 class Error(Exception):
     """Base class of most errors raised by the Pebble client."""
 
+    def __repr__(self):
+        return '<{}.{} {}>'.format(type(self).__module__, type(self).__name__, self.args)
+
+    def name(self):
+        """Return a string representation of the model plus class."""
+        return '<{}.{}>'.format(type(self).__module__, type(self).__name__)
+
+    def message(self):
+        """Return the message passed as an argument."""
+        return self.args[0]
+
 
 class TimeoutError(TimeoutError, Error):
     """Raised when a polling timeout occurs."""

diff --git a/test/test_model.py b/test/test_model.py
index 030393a..3f80664 100755
--- a/test/test_model.py
+++ b/test/test_model.py
@@ -1013,6 +1013,22 @@ containers:
             ('remove_path', '/path/2', True),
         ])
 
+    def test_no_exception_with_contextmanager(self):
+        with self.assertLogs() as logs:
+            self.pebble.responses.append('dummy')
+            with self.container.is_ready() as c:
+                raise ops.pebble.ConnectionError("Some dummy message")
+        self.assertIn("was raised due to", logs.records[0].getMessage())
+        self.assertEqual(c.completed, False)
+
+    def test_exception_without_contextmanager(self):
+        with self.assertRaises(ops.pebble.ConnectionError):
+            raise ops.pebble.ConnectionError("Some dummy message")
+
+    def test_bare_is_ready_call(self):
+        self.pebble.responses.append('dummy')
+        self.assertTrue(self.container.is_ready())
+
 
 class MockPebbleBackend(ops.model._ModelBackend):
     def get_pebble(self, socket_path):

Confusing / inconsistent errors from interaction between Pebble and Juju events and handlers
I've frequently seen charm authors who are new to the sidecar pattern write something along the lines of this in their charm:

```python
class MyCharm(CharmBase):
    def __init__(self, *args):
        super().__init__(*args)
        self.framework.observe(self.on.mycharm_pebble_ready, self._create_layer)
        self.framework.observe(self.on.config_changed, self._restart_service)

    def _create_layer(self, event):
        container = self.unit.get_container("mycharm")
        container.add_layer("mycharm", {"services": {"mycharm": {...}}})
        container.autostart()

    def _restart_service(self, event):
        container = self.unit.get_container("mycharm")
        # presumably introspect and / or update the service, then...
        container.stop("mycharm")
        container.start("mycharm")
```

This seems like a straightforward and reasonable way for someone to start out approaching this, but can result in any one of 5 different outcomes:

  * It might work fine.  Depending on timing and how the `_restart_service` handler does or doesn't implement updating the service based on config changes, this might deploy and run fine, at least most of the time.

  * It might raise `ops.pebble.ConnectionError`.  Depending on how quickly Pebble becomes ready to accept connections, it might fail trying to talk to Pebble at all. Worse, this could be an intermittent failure. Additionally, this won't ever happen in unit tests because the `_TestingPebbleClient` is always ready immediately.

  * It might raise `RuntimeError: 400 Bad Request: service "mycharm" does not exist`. This is somewhat related to #514 but is slightly different and might be specific to the `_TestingPebbleClient`.

  * It might raise `ops.pebble.APIError: 400 Bad Request: service "mycharm" does not exist`. A charm that raised the previous `RuntimeError` during unit tests would most likely raise this during an actual deployment.

  * It might raise `ops.model.ModelError: service 'mycharm' not found`. If the `_restart_service` handler does an explicit `container.get_service("mycharm")`, then it will get this rather than either of the previous two errors, unless it calls `container.add_layer(layer_name, layer_definition, combine=True)` first.

It would at least be good to ensure that all of the latter 3 cases result in a single `ops.pebble.UnknownServiceError` or something, but I've found that new charmers will still be confused as to why the service isn't recognized despite them having defined it during the `pebble-ready` event. Maybe the additional message on the `UnknownServiceError` can include a hint such as `(typically due to referencing a service before add_layer is called)`.

It would also be good to make the testing harness always raise `ConnectionError` until the `pebble-ready` event is triggered, to force charm authors to consider that possibility.

A `container.restart(service)` helper would be nice, as well.
test/test_model.py::TestContainerPebble::test_bare_is_ready_call test/test_model.py::TestContainerPebble::test_no_exception_with_contextmanager
test/test_model.py::TestModel::test_active_message_default test/test_model.py::TestModel::test_app_immutable test/test_model.py::TestModel::test_app_relation_data_modify_local_as_leader test/test_model.py::TestModel::test_app_relation_data_modify_local_as_minion test/test_model.py::TestModel::test_base_status_instance_raises test/test_model.py::TestModel::test_config test/test_model.py::TestModel::test_config_immutable test/test_model.py::TestModel::test_get_relation test/test_model.py::TestModel::test_is_leader test/test_model.py::TestModel::test_local_set_valid_app_status test/test_model.py::TestModel::test_local_set_valid_unit_status test/test_model.py::TestModel::test_model_attributes test/test_model.py::TestModel::test_model_name_from_backend test/test_model.py::TestModel::test_our_unit_is_our test/test_model.py::TestModel::test_peer_relation_app test/test_model.py::TestModel::test_pod_immutable test/test_model.py::TestModel::test_pod_spec test/test_model.py::TestModel::test_relation_data_del_key test/test_model.py::TestModel::test_relation_data_del_missing_key test/test_model.py::TestModel::test_relation_data_modify_our test/test_model.py::TestModel::test_relation_data_modify_remote test/test_model.py::TestModel::test_relation_data_type_check test/test_model.py::TestModel::test_relation_no_units test/test_model.py::TestModel::test_relation_set_fail test/test_model.py::TestModel::test_relations_immutable test/test_model.py::TestModel::test_relations_keys test/test_model.py::TestModel::test_remote_app_relation_data test/test_model.py::TestModel::test_remote_app_status test/test_model.py::TestModel::test_remote_unit_status test/test_model.py::TestModel::test_remote_units_is_our test/test_model.py::TestModel::test_resources test/test_model.py::TestModel::test_resources_immutable test/test_model.py::TestModel::test_set_app_status_invalid test/test_model.py::TestModel::test_set_app_status_non_leader_raises test/test_model.py::TestModel::test_set_unit_status_invalid test/test_model.py::TestModel::test_status_eq test/test_model.py::TestModel::test_status_repr test/test_model.py::TestModel::test_storage test/test_model.py::TestModel::test_storages_immutable test/test_model.py::TestModel::test_unit_immutable test/test_model.py::TestModel::test_unit_relation_data test/test_model.py::TestModel::test_workload_version test/test_model.py::TestModel::test_workload_version_invalid test/test_model.py::TestContainers::test_unit_containers test/test_model.py::TestContainers::test_unit_get_container test/test_model.py::TestContainerPebble::test_add_layer test/test_model.py::TestContainerPebble::test_autostart test/test_model.py::TestContainerPebble::test_exception_without_contextmanager test/test_model.py::TestContainerPebble::test_get_plan test/test_model.py::TestContainerPebble::test_get_service test/test_model.py::TestContainerPebble::test_get_services test/test_model.py::TestContainerPebble::test_list_files test/test_model.py::TestContainerPebble::test_make_dir test/test_model.py::TestContainerPebble::test_pull test/test_model.py::TestContainerPebble::test_push test/test_model.py::TestContainerPebble::test_remove_path test/test_model.py::TestContainerPebble::test_socket_path test/test_model.py::TestContainerPebble::test_start test/test_model.py::TestContainerPebble::test_start_no_arguments test/test_model.py::TestContainerPebble::test_stop test/test_model.py::TestContainerPebble::test_stop_no_arguments test/test_model.py::TestContainerPebble::test_type_errors test/test_model.py::TestModelBindings::test_binding_by_relation test/test_model.py::TestModelBindings::test_binding_by_relation_name test/test_model.py::TestModelBindings::test_binding_no_iface_name test/test_model.py::TestModelBindings::test_dead_relations test/test_model.py::TestModelBindings::test_empty_bind_addresses test/test_model.py::TestModelBindings::test_empty_interface_info test/test_model.py::TestModelBindings::test_invalid_keys test/test_model.py::TestModelBindings::test_missing_bind_addresses test/test_model.py::TestModelBindings::test_missing_egress_subnets test/test_model.py::TestModelBindings::test_missing_ingress_addresses test/test_model.py::TestModelBindings::test_no_bind_addresses test/test_model.py::TestModelBackend::test_action_fail test/test_model.py::TestModelBackend::test_action_get test/test_model.py::TestModelBackend::test_action_get_error test/test_model.py::TestModelBackend::test_action_log test/test_model.py::TestModelBackend::test_action_log_error test/test_model.py::TestModelBackend::test_action_set test/test_model.py::TestModelBackend::test_action_set_error test/test_model.py::TestModelBackend::test_application_version_set test/test_model.py::TestModelBackend::test_application_version_set_invalid test/test_model.py::TestModelBackend::test_invalid_metric_label_values test/test_model.py::TestModelBackend::test_invalid_metric_labels test/test_model.py::TestModelBackend::test_invalid_metric_names test/test_model.py::TestModelBackend::test_invalid_metric_values test/test_model.py::TestModelBackend::test_is_leader_refresh test/test_model.py::TestModelBackend::test_juju_log test/test_model.py::TestModelBackend::test_local_set_invalid_status test/test_model.py::TestModelBackend::test_network_get test/test_model.py::TestModelBackend::test_network_get_errors test/test_model.py::TestModelBackend::test_relation_get_juju_version_quirks test/test_model.py::TestModelBackend::test_relation_get_set_is_app_arg test/test_model.py::TestModelBackend::test_relation_remote_app_name_env test/test_model.py::TestModelBackend::test_relation_remote_app_name_script_errors test/test_model.py::TestModelBackend::test_relation_remote_app_name_script_success test/test_model.py::TestModelBackend::test_relation_set_juju_version_quirks test/test_model.py::TestModelBackend::test_relation_tool_errors test/test_model.py::TestModelBackend::test_status_get test/test_model.py::TestModelBackend::test_status_is_app_forced_kwargs test/test_model.py::TestModelBackend::test_status_set_is_app_not_bool_raises test/test_model.py::TestModelBackend::test_storage_tool_errors test/test_model.py::TestModelBackend::test_valid_metrics test/test_model.py::TestLazyMapping::test_invalidate