【问题标题】:Are there problems using Try or Either for error handling in public/open source Scala APIs?在公共/开源 Scala API 中使用 Try 或 Either 进行错误处理是否存在问题?
【发布时间】:2026-01-13 12:05:02
【问题描述】:

我注意到许多开源 Scala API 使用 Future(如 Slick)或 Java 样式的异常处理(如 spray-json),而不是 TryEither

在非长时间运行或异步的简单开源 API 中返回 TryEither 是否存在问题?我应该避免这些,如果是,我应该改用什么?

(显然,我更喜欢使用核心 Scala API 而不是第三方库,例如 Scalaz,以避免迫使下游用户也使用这些库。)

【问题讨论】:

  • 我认为这太宽泛了,这取决于您的用例,在生产应用程序中您可能希望使用Either 之类的东西(或者我宁愿使用Disjunctions 或Validation s) 更好地处理如何响应用户。
  • @EndeNeu 我已将问题更新为更具体:这是关于不长时间运行或需要异步的小型、简单的 OSS 库。我还希望在核心库中返回类型:例如,我不希望我的下游用户必须处理 scalaz,除非他们愿意。
  • 我认为TryEither 对于某些用途可能有一些奇怪的语义,尽管我不太确定。例如,我过去在理解中使用Try 时遇到了一些麻烦(*.com/questions/15015495/…),而关于Either,它不带偏见的事实也可能很尴尬(blog.vngrs.com/right-biased-either-in-scala)。不过,您也可以在这些标准类上捆绑您自己的基本语义。
  • @ale64bit 你会用什么代替?
  • 正如@EndeNeu 提到的,类似scalaz 或github.com/wix/accord。我相信有几种选择。但就我而言,我会尝试使用标准类并实现我自己的语义。大多数时候,我不需要任何复杂的东西,并且由于隐含,上述库(特别是 scalaz)在大型项目中会产生编译时间成本。

标签: scala


【解决方案1】:

Try 非常适合包装非异步操作的异常。
但是,我认为使用异常实例返回非异常错误条件并不正确。当 API 是异步的并使用 Future 时,我也坚持这一点。 将 exception 错误与 business 错误混合很容易导致 API 的客户端(又名我 :) 错误处理不正确/不充分)。

如果您决定仍然使用异常来表示您的业务错误,请记住异常有一个隐藏的代价:构建堆栈跟踪。如果您创建自己的业务异常并混合scala.util.control.NoStackTrace,则可以缓解这种情况。

Scala 标准库中默认的Either 实现是无偏见的,这意味着区分成功和失败案例仅依赖于约定:

右就是对(正确),所以左就是错(不正确)。

公正的一面也让遵循“快乐”的道路(或使错误恢复脱颖而出)变得痛苦

此时您的最佳选择(scala 2.11.6 和相应的标准库)将根据您的依赖项和您希望向用户公开的 API 而有所不同,但它不会来自 scala 标准库。

  • 如果你已经严重依赖 scalaz,Scalaz Validation \/ 是一个选择
  • 如果您不想引入整个 scalaz,Scalactic's Or type 是一个不错的轻量级 解决方案。它是成熟的,是从 scalatest 中提取的
  • (来自 cmets)Accord 可能是一个选项,但我对此了解不多,无法与其他选项进行比较。
  • (来自 cmets)Cat's Xor 可能是一个选项,但我对它的了解还不够,无法与其他选项进行比较。
  • Rapture modes 可以让您让 API 的客户端决定他想要返回的类型(例如在 parboiled2 中使用这种方法)。我不知道图书馆作者或编译时间的成本是多少。

您可能还依赖于提供自己的 validation 类型的库。例如,play-json 有一个 JsResult 类型,具有类似验证的能力。根据您的工作,重用一个类型可能是有意义的。

有一个slip 正在进行中,还有一个expert group call 用于向Scala 中的标准库添加偏向的任一类型。上述 2 个解决方案的额外好处是已经能够累积错误。

【讨论】:

  • 感谢您提供信息丰富的答案,但我认为对于这样的公共 API 而言,使用非核心 Scala API 类型将是一个问题:我更愿意对下游用户隐藏 scalaz。对于 Try 或 Future 之类的错误返回类型,我也不太在意使用异常。
  • 我添加了关于使用业务异常和在 NoStackTrace 中混合以降低性能成本的注释。请小心使用,因为禁用堆栈跟踪会使代码难以调试:)
  • 有趣的 NoStackTrace 功能,不知道它存在
  • "但是,我认为使用异常实例返回非异常错误条件是不正确的" -> 你是对的,它不干净;但只要创建扩展Throwable 的自定义业务错误,异常部分就会被隐藏并且不会弄乱他们的代码。我认为仅仅因为Try 的签名中有Exception 就使用外部库是矫枉过正的。
  • 您不想要 Scalaz \/ 而不是验证吗?第四个选项:猫的Xor
【解决方案2】:

在以下情况下返回 Try[T]:正确结果和错误结果之间存在语义差异(即错误)。这是因为:

  • Try 偏右(适用于地图、平面地图和用于理解)。

  • 失败案例必须是 Throwable,这很重要,因为 throwable 总是清楚地表明即使超出代码边界也有问题。

警告:确实,堆栈跟踪生成有时是一种开销,但并非总是如此!在过早地优化代码之前要三思:堆栈跟踪带有很多有用的调试信息。 如果你以后发现你真的需要跳过堆栈跟踪生成,没问题:使用with NoStackTrace,如上所示。

def validate(s:String):Try[String] = Option(s).filter(!_.isEmpty).map(Try(_)).getOrElse(new Exception("bad input") with NoStackTrace 

时使用 Either[L,R]:这两种情况都可以接受,但它们是互斥的(即 Either[Integer,Float])。出于这个原因,尽管 Scalaz 的家伙对此有什么看法,但我相信 Either 没有偏见是可以的。

def parseNum(s:String):Either[Int,String] = Try(parseInt(s)).map(Left(_)).getOrElse(Right(s))

类似于 Either 时使用元组,但是当你有 N 个非互斥值时(你知道所有这些值都会有用)

def parseAndReturn(s:String):(Integer,String) = (parseInt(s), s) 

T 可以为空,或者当您处于与 Try[T] 相同的情况,但您没有给出 s*** 时,返回 Option[T] t 关于为什么会出错。

def attemptToparse(s:String):Option[Result] = Try(parse(s)).toOption

【讨论】:

  • 关于您对Either 没有偏见的接受度:您能否扩展一下您认为对于Either[Int, Float] 用例会消除的偏见?我同意这种情况不需要它,但它其他人需要的,不是吗?
  • 当然:即使无偏见的 Either 的应用领域较少,这也确保了您仅在两种类型对您的逻辑的快乐路径同样有效时才使用 Either。如果您的左侧不代表一条幸福的道路,那么您正在滥用此工具,而 Try 是适合您的工具(如果您关心坚持标准 Scala)。如果您有自定义错误,为什么不扩展 Exception with NoStackTrace 并坚持尝试?
【解决方案3】:

我同意Try 用于捕获/具体化异常,而不是用于返回业务/验证错误。 Scalaz 是一个很好的选择,但如果你不想依赖它,我认为Either 没问题,不偏不倚很烦人,但恕我直言: 一旦你确立了 Right 是有效结果的约定,你可以使用.rightProjection“右偏”它(如果你只想处理错误,你可以做一个左投影。 另一种选择是使用 Either 的折叠 fold[X](fa: (A) ⇒ X, fb: (B) ⇒ X): XmyEither.fold(dealWithErrors _, happyPath _ ) (尽管这意味着 dealWithErrorshappyPath 返回相同的类型)

【讨论】:

    最近更新 更多