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
相关产品推荐
相关产品推荐

