diff --git a/modules/order/src/OrderStorage.php b/modules/order/src/OrderStorage.php
index dd2389b6e55213fc6a6c6383b2ebfd00c6524e51..38f36e56161bbacb109dad78826fd33dbc848575 100644
--- a/modules/order/src/OrderStorage.php
+++ b/modules/order/src/OrderStorage.php
@@ -170,10 +170,7 @@ class OrderStorage extends CommerceContentEntityStorage implements OrderStorageI
     }
     finally {
       // Release the update lock if it was acquired for this entity.
-      if (isset($this->updateLocks[$entity->id()])) {
-        $this->lockBackend->release($this->getLockId($entity->id()));
-        unset($this->updateLocks[$entity->id()]);
-      }
+      $this->releaseLock($entity->id());
     }
   }
 
@@ -209,4 +206,14 @@ class OrderStorage extends CommerceContentEntityStorage implements OrderStorageI
     return 'commerce_order_update:' . $order_id;
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  public function releaseLock(int $order_id): void {
+    if (isset($this->updateLocks[$order_id])) {
+      $this->lockBackend->release($this->getLockId($order_id));
+      unset($this->updateLocks[$order_id]);
+    }
+  }
+
 }
diff --git a/modules/order/src/OrderStorageInterface.php b/modules/order/src/OrderStorageInterface.php
index 34fabb7eebcdec2d54459f2a6e9c0a7429f10e78..9efc59818587290a14a37722a20cba78692c80fa 100644
--- a/modules/order/src/OrderStorageInterface.php
+++ b/modules/order/src/OrderStorageInterface.php
@@ -31,4 +31,20 @@ interface OrderStorageInterface extends ContentEntityStorageInterface {
    */
   public function loadForUpdate(int $order_id): ?OrderInterface;
 
+  /**
+   * Release the order lock.
+   *
+   * In the normal scenario, the lock will be released automatically when the
+   * order is saved. There may be times, however, when we want to release the
+   * lock without a save. One is if we waited for the lock and then determined
+   * the order is not in a state we can't process. e.g. We wanted to process
+   * a draft order, but another process has completed or cancelled the order.
+   * Another scenario is if an exception occurs, in which case, we want to
+   * release the lock.
+   *
+   * @param int $order_id
+   *   The order ID.
+   */
+  public function releaseLock(int $order_id): void;
+
 }
diff --git a/modules/payment/src/Controller/PaymentCheckoutController.php b/modules/payment/src/Controller/PaymentCheckoutController.php
index f4bd26aaf90e5035882b02e8eb652741accea872..16df5413dde0d85250ebedaa2bf98abf630b8185 100644
--- a/modules/payment/src/Controller/PaymentCheckoutController.php
+++ b/modules/payment/src/Controller/PaymentCheckoutController.php
@@ -112,47 +112,61 @@ class PaymentCheckoutController implements ContainerInjectionInterface {
     $step_id = $route_match->getParameter('step');
     $this->validateStepId($step_id, $order);
 
-    // Reload the order and mark it for updating, redirecting to step below
-    // will save it and free the lock. This must be done before the checkout
-    // flow plugin is initiated to make sure that it has the reloaded order
-    // object. Additionally, the checkout flow plugin gets the order from
-    // the route match object, so update the order there as well with. The
-    // passed in route match object is created on-demand in
-    // \Drupal\Core\Controller\ArgumentResolver\RouteMatchValueResolver and is
-    // not the same object as the current route match service.
-    $order = $this->entityTypeManager->getStorage('commerce_order')->loadForUpdate($order->id());
-    \Drupal::routeMatch()->getParameters()->set('commerce_order', $order);
-
-    /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */
-    $payment_gateway = $order->get('payment_gateway')->entity;
-    $payment_gateway_plugin = $payment_gateway->getPlugin();
-    if (!$payment_gateway_plugin instanceof OffsitePaymentGatewayInterface) {
-      throw new AccessException('The payment gateway for the order does not implement ' . OffsitePaymentGatewayInterface::class);
-    }
-    /** @var \Drupal\commerce_checkout\Entity\CheckoutFlowInterface $checkout_flow */
-    $checkout_flow = $order->get('checkout_flow')->entity;
-    $checkout_flow_plugin = $checkout_flow->getPlugin();
-
+    /** @var \Drupal\commerce_order\OrderStorageInterface $order_storage */
+    $order_storage = $this->entityTypeManager->getStorage('commerce_order');
     try {
-      $payment_gateway_plugin->onReturn($order, $request);
-      $redirect_step_id = $checkout_flow_plugin->getNextStepId($step_id);
-    }
-    catch (NeedsRedirectException $e) {
-      throw $e;
-    }
-    catch (PaymentGatewayException $e) {
-      $event = new FailedPaymentEvent($order, $payment_gateway, $e);
-      $this->eventDispatcher->dispatch($event, PaymentEvents::PAYMENT_FAILURE);
-      Error::logException($this->logger, $e);
-      $this->messenger->addError(t('Payment failed at the payment server. Please review your information and try again.'));
-      $redirect_step_id = $checkout_flow_plugin->getPreviousStepId($step_id);
+      // Reload the order and mark it for updating, redirecting to step below
+      // will save it and free the lock. This must be done before the checkout
+      // flow plugin is initiated to make sure that it has the reloaded order
+      // object. Additionally, the checkout flow plugin gets the order from
+      // the route match object, so update the order there as well with. The
+      // passed in route match object is created on-demand in
+      // \Drupal\Core\Controller\ArgumentResolver\RouteMatchValueResolver and is
+      // not the same object as the current route match service.
+      /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
+      $order = $order_storage->loadForUpdate($order->id());
+      \Drupal::routeMatch()->getParameters()->set('commerce_order', $order);
+      if ($order->getState()->getId() !== 'draft') {
+        // While we waited for the lock, the order state changed.
+        // Release our lock, and revalidate the step.
+        $order_storage->releaseLock($order->id());
+        $this->validateStepId($step_id, $order);
+      }
+
+      /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */
+      $payment_gateway = $order->get('payment_gateway')->entity;
+      $payment_gateway_plugin = $payment_gateway->getPlugin();
+      if (!$payment_gateway_plugin instanceof OffsitePaymentGatewayInterface) {
+        throw new AccessException('The payment gateway for the order does not implement ' . OffsitePaymentGatewayInterface::class);
+      }
+      /** @var \Drupal\commerce_checkout\Entity\CheckoutFlowInterface $checkout_flow */
+      $checkout_flow = $order->get('checkout_flow')->entity;
+      $checkout_flow_plugin = $checkout_flow->getPlugin();
+
+      try {
+        $payment_gateway_plugin->onReturn($order, $request);
+        $redirect_step_id = $checkout_flow_plugin->getNextStepId($step_id);
+      }
+      catch (NeedsRedirectException $e) {
+        throw $e;
+      }
+      catch (PaymentGatewayException $e) {
+        $event = new FailedPaymentEvent($order, $payment_gateway, $e);
+        $this->eventDispatcher->dispatch($event, PaymentEvents::PAYMENT_FAILURE);
+        Error::logException($this->logger, $e);
+        $this->messenger->addError(t('Payment failed at the payment server. Please review your information and try again.'));
+        $redirect_step_id = $checkout_flow_plugin->getPreviousStepId($step_id);
+      }
+      catch (\Exception $e) {
+        Error::logException($this->logger, $e);
+        $this->messenger->addError(t('We encountered an issue recording your payment. Please contact customer service to resolve the issue.'));
+        $redirect_step_id = $checkout_flow_plugin->getPreviousStepId($step_id);
+      }
+      $checkout_flow_plugin->redirectToStep($redirect_step_id);
     }
-    catch (\Exception $e) {
-      Error::logException($this->logger, $e);
-      $this->messenger->addError(t('We encountered an issue recording your payment. Please contact customer service to resolve the issue.'));
-      $redirect_step_id = $checkout_flow_plugin->getPreviousStepId($step_id);
+    finally {
+      $order_storage->releaseLock($order->id());
     }
-    $checkout_flow_plugin->redirectToStep($redirect_step_id);
   }
 
   /**