diff --git a/extension/reviewbotext/admin.py b/extension/reviewbotext/admin.py
index 49989cde034b53b62b7ee0555140c557d7ea13ed..865b1bc04e4e623e00c7ac05601da862bd5ce543 100644
--- a/extension/reviewbotext/admin.py
+++ b/extension/reviewbotext/admin.py
@@ -1,7 +1,7 @@
 """Admin site definitions."""
 
 from django.contrib import admin
-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseServerError
 from django.template.context import RequestContext
 from django.urls import path
 from reviewboard.extensions.base import get_extension_manager
@@ -69,7 +69,12 @@ class ToolAdmin(admin.ModelAdmin):
             The result of the API call.
         """
         Tool.objects.all().update(in_last_update=False)
-        ReviewBotExtension.instance.send_refresh_tools()
+
+        try:
+            ReviewBotExtension.instance.send_refresh_tools()
+        except Exception as e:
+            # This is already logged.
+            return HttpResponseServerError(str(e))
 
         # The caller is not sensitive to the contents. It's just looking for
         # a response.
diff --git a/extension/reviewbotext/extension.py b/extension/reviewbotext/extension.py
index d1857a15c85321c0a748dacf87ca08ebcb9b0974..adc1c41bff989156e92a0a698a6f7a25a4a0df17 100644
--- a/extension/reviewbotext/extension.py
+++ b/extension/reviewbotext/extension.py
@@ -151,6 +151,12 @@ class ReviewBotExtension(Extension):
         }
 
         with log_timed('Refreshing Review Bot tools list',
-                       logger=logger):
-            self.celery.control.broadcast('update_tools_list',
-                                          payload=payload)
+                       logger=logger) as log_timer:
+            try:
+                self.celery.control.broadcast('update_tools_list',
+                                              payload=payload)
+            except Exception as e:
+                logger.exception('[%s] Unexpected error fetching '
+                                 'Review Bot tools list: %s',
+                                 log_timer.trace_id, e)
+                raise
diff --git a/extension/reviewbotext/integration.py b/extension/reviewbotext/integration.py
index 0a900f5d92105d177392043a4d2bcc82f47dc438..77a2a7240a82860132e00fcde07b641e0a542f9d 100644
--- a/extension/reviewbotext/integration.py
+++ b/extension/reviewbotext/integration.py
@@ -5,6 +5,7 @@ from __future__ import annotations
 import json
 import logging
 from datetime import datetime
+from typing import TYPE_CHECKING
 
 from django.utils.functional import cached_property
 from django.utils.translation import gettext_lazy as _
@@ -19,6 +20,9 @@ from reviewbotext.compat.logs import log_timed
 from reviewbotext.forms import ReviewBotConfigForm
 from reviewbotext.models import Tool
 
+if TYPE_CHECKING:
+    from reviewboard.scmtools.models import Repository
+
 
 logger = logging.getLogger(__name__)
 
@@ -203,32 +207,43 @@ class ReviewBotIntegration(Integration):
                 status_update.save()
 
                 repository = review_request.repository
-                queue = '%s.%s' % (tool.entry_point, tool.version)
-
-                if tool.working_directory_required:
-                    queue = '%s.%s' % (queue, repository.name)
+                queue = self._build_queue(repository=repository,
+                                          tool=tool)
 
                 with log_timed(f'Sending automatic run task to Review Bot '
                                f'queue {queue} for review request '
                                f'{review_request.pk}, diff revision '
                                f'{diffset.revision}, status update ID '
                                f'{status_update.pk}',
-                               logger=logger):
-                    extension.celery.send_task(
-                        'reviewbot.tasks.RunTool',
-                        kwargs={
-                            'server_url': server_url,
-                            'session': session,
-                            'username': user.username,
-                            'review_request_id': review_request_id,
-                            'diff_revision': diffset.revision,
-                            'status_update_id': status_update.pk,
-                            'review_settings': review_settings,
-                            'tool_options': tool_options,
-                            'repository_name': repository.name,
-                            'base_commit_id': diffset.base_commit_id,
-                        },
-                        queue=queue)
+                               logger=logger) as log_timer:
+                    try:
+                        extension.celery.send_task(
+                            'reviewbot.tasks.RunTool',
+                            kwargs={
+                                'server_url': server_url,
+                                'session': session,
+                                'username': user.username,
+                                'review_request_id': review_request_id,
+                                'diff_revision': diffset.revision,
+                                'status_update_id': status_update.pk,
+                                'review_settings': review_settings,
+                                'tool_options': tool_options,
+                                'repository_name': repository.name,
+                                'base_commit_id': diffset.base_commit_id,
+                            },
+                            queue=queue)
+                    except Exception as e:
+                        logger.exception(
+                            '[%s] Unexpected error invoking manual '
+                            'Review Bot tool run on Review Bot queue '
+                            '%s for review request %s, diff revision '
+                            '%s, status update ID %s: %s',
+                            log_timer.trace_id, queue, review_request.pk,
+                            diffset.revision, status_update.pk, e)
+
+                        self._update_for_tool_error(
+                            status_update=status_update,
+                            error_id=log_timer.trace_id)
 
     def _drop_old_issues(self, user, service_id, review_request):
         """Drop old issues associated with the given tool config.
@@ -302,13 +317,10 @@ class ReviewBotIntegration(Integration):
         status_update.timestamp = datetime.now()
         status_update.save(update_fields=('description', 'state', 'timestamp'))
 
-        repository = review_request.repository
-        queue = '%s.%s' % (tool.entry_point, tool.version)
-
-        if tool.working_directory_required:
-            queue = '%s.%s' % (queue, repository.name)
-
         changedesc = status_update.change_description
+        repository = review_request.repository
+        queue = self._build_queue(repository=repository,
+                                  tool=tool)
 
         # If there's a change description associated with the status
         # update, then use the diff from that. Otherwise, choose the first
@@ -330,19 +342,86 @@ class ReviewBotIntegration(Integration):
                        f'for review request {review_request.pk}, '
                        f'diff revision {diffset.revision}, '
                        f'status update ID {status_update.pk}',
-                       logger=logger):
-            extension.celery.send_task(
-                'reviewbot.tasks.RunTool',
-                kwargs={
-                    'server_url': server_url,
-                    'session': session,
-                    'username': user.username,
-                    'review_request_id': review_request.get_display_id(),
-                    'diff_revision': diffset.revision,
-                    'status_update_id': status_update.pk,
-                    'review_settings': review_settings,
-                    'tool_options': tool_options,
-                    'repository_name': repository.name,
-                    'base_commit_id': diffset.base_commit_id,
-                },
-                queue=queue)
+                       logger=logger) as log_timer:
+            try:
+                extension.celery.send_task(
+                    'reviewbot.tasks.RunTool',
+                    kwargs={
+                        'server_url': server_url,
+                        'session': session,
+                        'username': user.username,
+                        'review_request_id': review_request.get_display_id(),
+                        'diff_revision': diffset.revision,
+                        'status_update_id': status_update.pk,
+                        'review_settings': review_settings,
+                        'tool_options': tool_options,
+                        'repository_name': repository.name,
+                        'base_commit_id': diffset.base_commit_id,
+                    },
+                    queue=queue)
+            except Exception as e:
+                logger.exception(
+                    '[%s] Unexpected error invoking manual Review Bot tool '
+                    'run on Review Bot queue %s for review request %s, '
+                    'diff revision %s, status update ID %s: %s',
+                    log_timer.trace_id, queue, review_request.pk,
+                    diffset.revision, status_update.pk, e)
+
+                self._update_for_tool_error(status_update=status_update,
+                                            error_id=log_timer.trace_id)
+
+    def _build_queue(
+        self,
+        *,
+        repository: Repository,
+        tool: Tool,
+    ) -> str:
+        """Return a queue name for a given tool and repository.
+
+        Version Added:
+            4.0.1
+
+        Args:
+            repository (reviewboard.scmtools.models.Repository):
+                The repository associated with the review request.
+
+            tool (reviewbotext.models.Tool):
+                The Review Bot tool to run.
+
+        Returns:
+            str:
+            The resulting queue name.
+        """
+        queue = f'{tool.entry_point}.{tool.version}'
+
+        if tool.working_directory_required:
+            queue = f'{queue}.{repository.name}'
+
+        return queue
+
+    def _update_for_tool_error(
+        self,
+        *,
+        status_update: StatusUpdate,
+        error_id: str,
+    ) -> None:
+        """Update a StatusUpdate to include a tool run error.
+
+        This is used when there's an error attempting to even run a tool.
+        This will display error information and include an error ID that can
+        be used to reference log files.
+
+        Version Added:
+            4.0.1
+
+        Args:
+            status_update (reviewboard.reviews.models.StatusUpdate):
+                The Status Update to update with the error information.
+
+            error_id (str):
+                The error ID to show in the message.
+        """
+        status_update.description = f'error running tool (error ID {error_id})'
+        status_update.state = StatusUpdate.ERROR
+        status_update.timestamp = datetime.now()
+        status_update.save(update_fields=('description', 'state', 'timestamp'))
diff --git a/extension/reviewbotext/templates/admin/reviewbotext/tool/change_list.html b/extension/reviewbotext/templates/admin/reviewbotext/tool/change_list.html
index 54bd82d77cbb5b28d2f1d14cc8d1753ac4679d35..ec713c856bf42a2bd100bedc27ee95151ac93eb0 100644
--- a/extension/reviewbotext/templates/admin/reviewbotext/tool/change_list.html
+++ b/extension/reviewbotext/templates/admin/reviewbotext/tool/change_list.html
@@ -1,32 +1,51 @@
 {% extends "admin/change_list.html" %}
+{% load i18n %}
 
 {% block scripts %}
-{{ block.super }}
-<script type="text/javascript">
+{{block.super}}
+<script>
 $(document).ready(function() {
-    $("#refresh_tools").bind('click',function(e) {
+    const $refreshTools = $('#refresh_tools');
+
+    $refreshTools.bind('click', e => {
         e.preventDefault();
-        $(this).unbind(e);
 
-        $("#refresh_tools")
-            .html('<span class="fa fa-spinner fa-pulse"></span> Refreshing...');
+        const origHTML = $refreshTools.html();
+
+{%  if version_raw.0 >= 7 %}
+        $refreshTools
+            .attr('aria-busy', 'true')
+            .text('{% trans "Refreshing..." %}');
+{%  else %}
+        $refreshTools
+            .html('<span class="djblets-o-spinner"></span> {% trans "Refreshing..." %}');
+{%  endif %}
+
+        $.ajax({
+            url: 'refresh/',
 
-        $.get('refresh/', function() {
-            window.setTimeout(function() {
-                location.reload()
-            }, 5000);
+            error: xhr => {
+                $refreshTools
+                    .attr('aria-busy', 'false')
+                    .html(origHTML);
+                alert('Unexpected error refreshing tools: ' +
+                      xhr.responseText);
+            },
+            success: () => {
+                window.setTimeout(() => location.reload(),
+                                  5000);
+            }
         });
     });
 });
 </script>
-{% endblock %}
+{% endblock scripts %}
 
 {% block object-tools %}
 {% if not is_popup %}
-  <ul class="object-tools">
-    <li>
-      <a id="refresh_tools" href="#">Refresh Installed Tools</a>
-    </li>
-  </ul>
+ <button class="rb-c-content-header__action {% if version_raw.0 >= 7 %}ink-c-button{% else %}rb-c-button{% endif %}" id="refresh_tools"
+         role="button">
+  {% trans "Refresh Installed Tools" %}
+ </button>
 {% endif %}
-{% endblock %}
+{% endblock object-tools %}
diff --git a/extension/reviewbotext/views.py b/extension/reviewbotext/views.py
index 9b533898a0f28be46b2e7d86d940d55309864a1f..ac1bb98f66e5ab865c309713ba480b2d8d4c6cac 100644
--- a/extension/reviewbotext/views.py
+++ b/extension/reviewbotext/views.py
@@ -284,12 +284,18 @@ class WorkerStatusView(View):
                 }
 
                 with log_timed('Fetching Review Bot worker status',
-                               logger=logger):
-                    reply = extension.celery.control.broadcast(
-                        'update_tools_list',
-                        payload=payload,
-                        reply=True,
-                        timeout=10)
+                               logger=logger) as log_timer:
+                    try:
+                        reply = extension.celery.control.broadcast(
+                            'update_tools_list',
+                            payload=payload,
+                            reply=True,
+                            timeout=10)
+                    except Exception as e:
+                        logger.exception('[%s] Unexpected error fetching '
+                                         'Review Bot worker status: %s',
+                                         log_timer.trace_id, e)
+                        raise
 
                 state = 'success'
                 error = None
@@ -329,7 +335,7 @@ class WorkerStatusView(View):
                     response['hosts'] = hosts
                 else:
                     response['error'] = error
-            except IOError as e:
+            except Exception as e:
                 response = {
                     'state': 'error',
                     'error': 'Unable to connect to broker: %s' % e,
