exceptionally仅捕获底层异常,不处理null或业务错误;handle统一处理结果与异常,需判空;容错链应分层拦截,exceptionally内勿抛异常。

exceptionally 只捕获上游异常,别指望它处理 null 或业务错误
它只在 completeExceptionally 被调用、或上游 lambda 抛出未捕获异常时触发。如果上游返回 null、false、-1 这类“逻辑失败”,exceptionally 完全不执行。
常见误用场景:
- HTTP 接口返回 404,却在解析响应时抛
RuntimeException再进exceptionally——这混淆了网络层异常和业务语义 - 数据库查不到记录,本该返回
Optional.empty(),却 throwNoResultException强行塞进异常流
真正该走 exceptionally 的,只有:ConnectException、JsonParseException、线程池拒绝、NullPointerException 在 IO 回调中意外发生等不可控底层问题。
handle 是唯一能同时看到 result 和 throwable 的统一入口
handle 无论成功失败都会执行,参数是 (T result, Throwable throwable),但必须手动判空——漏掉 throwable == null 判断,直接对 result 调用方法,立刻 NPE。
典型写法:
future.handle((result, throwable) -> {
if (throwable != null) {
return fallbackByError(throwable);
}
return validateAndWrap(result); // result 此时才可信
});
注意点:
- 即使上游成功,
handle也必然执行,比thenApply多一次函数调用,在高并发下可感知 - 它内部若抛异常,会继续向后传播,不会被吞掉(这点和
exceptionally不同) - 适合做统一日志、结果校验、空值兜底、缓存回源等需上下文联动的操作
容错链不是堆方法,而是分层拦截 + 明确职责
一个健壮的链路,往往需要多个异常处理节点协同,而不是靠单个 exceptionally 一劳永逸。
推荐组合模式:
- 在 IO 调用后立即接
exceptionally:处理连接超时、解析失败等底层异常,返回安全默认值(如空集合) - 在数据转换阶段(如
thenApply)后加handle:校验非空、字段合法性,对业务失败值做降级或包装 - 整个链末尾再补一层
exceptionally:兜底捕获前面所有漏掉的异常(比如handle里自己抛的新异常)
示例:
CompletableFuture.supplyAsync(() -> apiClient.getUser(uid))
.orTimeout(800, TimeUnit.MILLISECONDS)
.exceptionally(ex -> {
log.warn("API timeout or network error", ex);
return User.DEFAULT;
})
.thenApply(user -> enrichWithProfile(user)) // 可能 NPE 或校验失败
.handle((enriched, ex) -> {
if (ex != null) {
log.warn("enrich failed", ex);
return User.withFallbackProfile(enriched);
}
return enriched;
})
.exceptionally(ex -> {
log.error("final fallback triggered", ex);
return User.EMPTY;
});
容易被忽略的传播细节:exceptionally 内部抛异常会被吞掉
这是最隐蔽的坑:exceptionally 的 lambda 里如果抛异常,这个异常不会继续传播,而是被静默丢弃,最终结果为 null(或泛型类型的默认值,如 0、false)。
而 handle 里抛异常则会照常传播,后续仍可被下一个 exceptionally 捕获。
所以务必遵守:
-
exceptionally体内不要抛新异常,只做日志 + 返回安全值 - 需要重抛或转换异常,请改用
handle并显式 throw - 任何返回值都别为
null,尤其在exceptionally中——它不会触发空指针检查,下游直接崩
复杂点在于:异常路径和正常路径的语义边界必须清晰。一旦把业务失败伪装成异常,整个链的可观测性和调试成本就上去了。











