You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Scala EitherT+Future异常捕获问题:仅首个服务异常可被捕获

问题分析及解决方案

问题根源

你的代码里,外层try-catch只能捕获同步抛出的异常:只有burnService.execute在调用时(还没进入Future异步逻辑)同步抛出的异常才会被捕获;而earnService.execute是在for推导式的后续步骤中执行的,属于Future的异步执行上下文,它抛出的异常会让对应的Future进入失败状态,不会冒泡到外层的同步try-catch块中。

调换两个服务调用顺序后,只有第一个服务调用的同步异常会被捕获,后续异步步骤的异常依然无法被外层try-catch处理。

解决方法

要处理异步流程中的异常,得利用Future的错误处理方法(recover/recoverWith),或者将所有可能抛出异常的异步调用封装成EitherT时就处理异常,确保异常被转换成Left(APIError),而不是让Future失败。

方案1:为每个服务调用添加异常处理

修改每个EitherT的构造,将服务调用的异常捕获并转换成Left(APIError):

private def processSearchItems(
      processedValuesCtx: OffersProcessedValuesCtx
  ): Future[Either[APIError, Seq[OfferV3Response]]] = {
    implicit val baseInfo: BaseInfo = BaseInfo.apply(this)

    val searchResults = processedValuesCtx.req.requestWrapperInternal.offers.searchResults
    val result        = searchResults.map { searchResult =>
      val searchItem                = enrichSearchItemWithPricingTokenData(searchResult)
      val searchItemProcessedValues = buildProcessedValues(searchItem, processedValuesCtx)

      (for {
        burnResponse <- EitherT(
                          burnService.execute(processedValuesCtx, searchItemProcessedValues)
                            .recover {
                              case ex: Exception =>
                                logger.logError(
                                  rc.toBaseLog(
                                    s"Exception in burn service for offer :: ${searchResult.identifier} :: ${ex.getMessage}",
                                    Some(ex)
                                  )
                                )
                                Left(APIError("BURN_SERVICE_ERROR", ex.getMessage))
                            }
                        )
        enrichedProcessedValuesAfterBurn = updateItemProcessedValuesAfterBurn(processedValuesCtx, burnResponse, searchItemProcessedValues)
        earnResponse <- EitherT(
                          earnService.execute(processedValuesCtx, enrichedProcessedValuesAfterBurn)
                            .recover {
                              case ex: Exception =>
                                logger.logError(
                                  rc.toBaseLog(
                                    s"Exception in earn service for offer :: ${searchResult.identifier} :: ${ex.getMessage}",
                                    Some(ex)
                                  )
                                )
                                Left(APIError("EARN_SERVICE_ERROR", ex.getMessage))
                            }
                        )
        splitTenders <- EitherT(
                          splitTenderResponseTransformer
                            .buildSplitTenderResponse(burnResponse, earnResponse, processedValuesCtx, searchItemProcessedValues)
                            .recover {
                              case ex: Exception =>
                                logger.logError(
                                  rc.toBaseLog(
                                    s"Exception in split tender transformer for offer :: ${searchResult.identifier} :: ${ex.getMessage}",
                                    Some(ex)
                                  )
                                )
                                Left(APIError("SPLIT_TENDER_ERROR", ex.getMessage))
                            }
                        )
      } yield {
        val finalTenders = splitTenderResponseTransformer
          .removeChargeSplitsTooCloseApart(splitTenders, processedValuesCtx)
          .sortBy(_.burn.map(_.points))

        OfferV3Response(
          offerIdentifier = searchResult.identifier,
          priceInfo = PriceInfoV3ResponseWrapper(splitTenders = finalTenders),
          priceUsedToCalculate = getPriceUsedToCalculate(processedValuesCtx)
        )
      }).value
        // 最后处理整个流程的意外异常,确保返回默认响应
        .recover {
          case ex: Exception =>
            logger.logError(
              rc.toBaseLog(
                s"Unexpected exception for offer :: ${searchResult.identifier} :: ${ex.getMessage}",
                Some(ex)
              )
            )
            Right(resp)
        }
    }
    EitherUtils.sequenceOfFutureEither(result)
  }

方案2:封装通用异常处理工具方法

如果多个地方需要处理类似的服务调用异常,可以封装工具方法简化代码:

private def wrapServiceCall[T](call: => Future[Either[APIError, T]], errorCode: String, offerId: String): Future[Either[APIError, T]] = {
  call.recover {
    case ex: Exception =>
      logger.logError(
        rc.toBaseLog(
          s"Exception in $errorCode for offer :: $offerId :: ${ex.getMessage}",
          Some(ex)
        )
      )
      Left(APIError(errorCode, ex.getMessage))
  }
}

之后在代码中调用这个工具方法:

burnResponse <- EitherT(
                  wrapServiceCall(
                    burnService.execute(processedValuesCtx, searchItemProcessedValues),
                    "BURN_SERVICE_ERROR",
                    searchResult.identifier
                  )
                )

关键说明

  • 外层try-catch仅对同步执行的代码有效,异步代码(Future内部)的异常必须用Future的错误处理方法捕获。
  • 通过recover将异常转换成Left(APIError),可以让EitherT流程正常处理错误分支,最终通过sequenceOfFutureEither汇总结果。
  • 如果需要在异常发生时返回默认的Right(resp),可以在整个EitherT.value的Future上添加recover,确保无论发生什么异常,都能返回预期的Either结果。

内容的提问来源于stack exchange,提问作者Yalav

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 19:13:32