diff --git a/src/Plugin/AdvancedQueue/JobType/RecurringOrderClose.php b/src/Plugin/AdvancedQueue/JobType/RecurringOrderClose.php
index e169ca0..0e2cacc 100644
--- a/src/Plugin/AdvancedQueue/JobType/RecurringOrderClose.php
+++ b/src/Plugin/AdvancedQueue/JobType/RecurringOrderClose.php
@@ -106,6 +106,16 @@ class RecurringOrderClose extends JobTypeBase implements ContainerFactoryPluginI
       // might have changed their payment method since the last attempt.
       return $this->handleDecline($order, $exception, $job->getNumRetries());
     }
+    catch (\Exception $exception) {
+      // If something more general goes wrong, we assume it's not possible
+      // or desirable to retry.
+      $this->handleFailedOrder($order);
+      $order->save();
+
+      // Rethrow the exception so that the queue processor can log the job's
+      // failure with the exception's message.
+      throw $exception;
+    }
 
     return JobResult::success();
   }
@@ -136,11 +146,7 @@ class RecurringOrderClose extends JobTypeBase implements ContainerFactoryPluginI
       $retry_days = 0;
       $result = JobResult::success('Dunning complete, recurring order not paid.');
 
-      $transition = $order->getState()->getWorkflow()->getTransition('mark_failed');
-      $order->getState()->applyTransition($transition);
-      if ($billing_schedule->getUnpaidSubscriptionState() != 'active') {
-        $this->updateSubscriptions($order, $billing_schedule->getUnpaidSubscriptionState());
-      }
+      $this->handleFailedOrder($order);
     }
     // Subscribers can choose to send a dunning email.
     $event = new PaymentDeclinedEvent($order, $retry_days, $num_retries, $max_retries);
@@ -151,6 +157,24 @@ class RecurringOrderClose extends JobTypeBase implements ContainerFactoryPluginI
   }
 
   /**
+   * Handles an order whose payment has definitively failed.
+   *
+   * Note that this does not save the order; the caller needs to do this.
+   *
+   * @param \Drupal\commerce_order\Entity\OrderInterface $order
+   *   The order.
+   */
+  protected function handleFailedOrder(OrderInterface $order) {
+    $transition = $order->getState()->getWorkflow()->getTransition('mark_failed');
+    $order->getState()->applyTransition($transition);
+
+    $billing_schedule = $order->get('billing_schedule')->entity;
+    if ($billing_schedule->getUnpaidSubscriptionState() != 'active') {
+      $this->updateSubscriptions($order, $billing_schedule->getUnpaidSubscriptionState());
+    }
+  }
+
+  /**
    * Updates the recurring order's subscriptions to the new state.
    *
    * @param \Drupal\commerce_order\Entity\OrderInterface $order
diff --git a/tests/modules/commerce_recurring_test/commerce_recurring_test.info.yml b/tests/modules/commerce_recurring_test/commerce_recurring_test.info.yml
new file mode 100644
index 0000000..13d083c
--- /dev/null
+++ b/tests/modules/commerce_recurring_test/commerce_recurring_test.info.yml
@@ -0,0 +1,7 @@
+name: 'Commerce Recurring Test'
+type: module
+description: 'Test module for RetryTest.'
+package: Testing
+core: 8.x
+dependencies:
+  - commerce_recurring:commerce_recurring
diff --git a/tests/modules/commerce_recurring_test/commerce_recurring_test.module b/tests/modules/commerce_recurring_test/commerce_recurring_test.module
new file mode 100644
index 0000000..81c62c0
--- /dev/null
+++ b/tests/modules/commerce_recurring_test/commerce_recurring_test.module
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * @file
+ * Contains hook implementations for the Commerce Recurring Test module.
+ */
+
+use Drupal\commerce_recurring_test\Entity\ExceptionPaymentMethod;
+
+/**
+ * Implements hook_entity_type_build().
+ */
+function commerce_recurring_test_entity_type_build(array &$entity_types) {
+  /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
+
+  // Change the entity class of the payment method entity so we can have it
+  // throw an exception.
+  $entity_types['commerce_payment_method']->setClass(ExceptionPaymentMethod::class);
+}
diff --git a/tests/modules/commerce_recurring_test/src/Entity/ExceptionPaymentMethod.php b/tests/modules/commerce_recurring_test/src/Entity/ExceptionPaymentMethod.php
new file mode 100644
index 0000000..404f058
--- /dev/null
+++ b/tests/modules/commerce_recurring_test/src/Entity/ExceptionPaymentMethod.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\commerce_recurring_test\Entity;
+
+use Drupal\commerce_payment\Entity\PaymentMethod;
+
+/**
+ * Replacement commerce_payment_method entity class that throws an exception.
+ *
+ * We only need to override getPaymentGateway(), as it is the first call to the
+ * entity after it is loaded in RecurringOrderManager::closeOrder().
+ */
+class ExceptionPaymentMethod extends PaymentMethod {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPaymentGateway() {
+    // Throw an exception if the test tells us to via the state. We need this
+    // switch because ensureOrder() causes this method to be called, at which
+    // point we want things to behave normally.
+    if (\Drupal::state()->get('commerce_recurring_test.payment_method_throw')) {
+      throw new \Exception("This payment is failing dramatically!");
+    }
+
+    return parent::getPaymentGateway();
+  }
+
+}
diff --git a/tests/src/Kernel/RetryTest.php b/tests/src/Kernel/RetryTest.php
index d24f90f..02aac43 100644
--- a/tests/src/Kernel/RetryTest.php
+++ b/tests/src/Kernel/RetryTest.php
@@ -5,6 +5,7 @@ namespace Drupal\Tests\commerce_recurring\Kernel;
 use Drupal\advancedqueue\Job;
 use Drupal\commerce_price\Price;
 use Drupal\commerce_recurring\Entity\Subscription;
+use Drupal\commerce_recurring_test\Entity\ExceptionPaymentMethod;
 
 /**
  * @coversDefaultClass \Drupal\commerce_recurring\Plugin\AdvancedQueue\JobType\RecurringOrderClose
@@ -13,6 +14,24 @@ use Drupal\commerce_recurring\Entity\Subscription;
 class RetryTest extends RecurringKernelTestBase {
 
   /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'system',
+    'advancedqueue',
+    'path',
+    'profile',
+    'state_machine',
+    'commerce_order',
+    'commerce_payment',
+    'commerce_payment_example',
+    'commerce_product',
+    'commerce_recurring',
+    'entity_reference_revisions',
+    'commerce_recurring_test',
+  ];
+
+  /**
    * The recurring order manager.
    *
    * @var \Drupal\commerce_recurring\RecurringOrderManagerInterface
@@ -133,6 +152,68 @@ class RetryTest extends RecurringKernelTestBase {
   }
 
   /**
+   * @covers ::process
+   * @covers ::handleDecline
+   * @covers ::updateSubscriptions
+   */
+  public function testFailure() {
+    $payment_method = ExceptionPaymentMethod::create([
+      'type' => 'credit_card',
+      'payment_gateway' => 'example',
+    ]);
+    $payment_method->save();
+
+    $subscription = Subscription::create([
+      'type' => 'product_variation',
+      'store_id' => $this->store->id(),
+      'billing_schedule' => $this->billingSchedule,
+      'uid' => $this->user,
+      'purchased_entity' => $this->variation,
+      'title' => $this->variation->getOrderItemTitle(),
+      'unit_price' => new Price('2', 'USD'),
+      'state' => 'active',
+      'starts' => strtotime('2017-02-24 17:00'),
+      'payment_method' => $payment_method,
+    ]);
+    $subscription->save();
+    $order = $this->recurringOrderManager->ensureOrder($subscription);
+
+    // Rewind time to the end of the first subscription.
+    $this->rewindTime(strtotime('2017-02-24 19:00'));
+    $job = Job::create('commerce_recurring_order_close', [
+      'order_id' => $order->id(),
+    ]);
+    $this->queue->enqueueJob($job);
+
+    // Tell the payment method entity class to throw an exception.
+    // This will be caught by RecurringOrderClose as with a DeclineException,
+    // but re-thrown, and then caught by processJob().
+    \Drupal::state()->set('commerce_recurring_test.payment_method_throw', TRUE);
+
+    $job = $this->queue->getBackend()->claimJob();
+    /** @var \Drupal\advancedqueue\ProcessorInterface $processor */
+    $processor = \Drupal::service('advancedqueue.processor');
+    $result = $processor->processJob($job, $this->queue);
+
+    // Confirm that the order was marked as failed.
+    $order = $this->reloadEntity($order);
+    $this->assertEquals('failed', $order->getState()->value);
+
+    // Confirm that the job result is correct.
+    $this->assertEquals(Job::STATE_FAILURE, $job->getState());
+    $this->assertEquals('This payment is failing dramatically!', $result->getMessage());
+
+    // Confirm that the job was not requeued.
+    $this->assertEquals(0, $job->getNumRetries());
+    $counts = array_filter($this->queue->getBackend()->countJobs());
+    $this->assertEquals([Job::STATE_FAILURE => 1], $counts);
+
+    // Confirm that the subscription was canceled.
+    $subscription = $this->reloadEntity($subscription);
+    $this->assertEquals('canceled', $subscription->getState()->value);
+  }
+
+  /**
    * {@inheritdoc}
    */
   protected function rewindTime($new_time) {
