8

I'm getting this exception:

Caused by: java.lang.NullPointerException
at java.util.concurrent.CompletableFuture.uniApply(CompletableFuture.java:616) ~[?:1.8.0_302]
at java.util.concurrent.CompletableFuture$UniApply.tryFire(CompletableFuture.java:591) ~[?:1.8.0_302]
at java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:488) ~[?:1.8.0_302]
at java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:1975) ~[?:1.8.0_302]
at com.tcom.concurrent.ConcurrentUtils$3.onSuccess(ConcurrentUtils.java:140) ~[framework-20220815.38-RELEASE.jar:?]

This exception is rare and not reproducible in my system.

Looking at CompletableFuture.java it seems the f Function variable is null.

But there is a null check for it in the second line of uniApply(), so f cannot be null.

So why the JVM claims I have a NPE on the function invocation line? If the NPE is coming from within the invoked function, shouldn't I see it in the stack trace?


The relevant part from ConcurrentUtils.java:

public static <I, O> CompletableFuture<O> buildCompletableFuture(final ListenableFuture<I> listenableFuture,
                                                                 final Function<I, O> responseApplier,
                                                                 final Consumer<I> onSuccessConsumer,
                                                                 final Consumer<Throwable> onFailureConsumer,
                                                                 final Supplier<O> defaultValueOnExceptionSupplier,
                                                                 final Executor callBackExecutor) {
    //create an instance of CompletableFuture
    final CompletableFuture<I> innerComplete = new CompletableFuture<I>() {
        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            // propagate cancel to the listenable future
            boolean result = listenableFuture.cancel(mayInterruptIfRunning);
            super.cancel(mayInterruptIfRunning);
            return result;
        }
    };

    // add callback
    Futures.addCallback(listenableFuture, new FutureCallback<I>() {
        @Override
        public void onSuccess(I result) {
            innerComplete.complete(result);
            if (onSuccessConsumer != null) {
                onSuccessConsumer.accept(result);
            }
        }

        @Override
        public void onFailure(Throwable t) {
            innerComplete.completeExceptionally(t);
            if (onFailureConsumer != null) {
                onFailureConsumer.accept(t);
            }
        }
    }, callBackExecutor);

    CompletableFuture<O> returnedFuture = innerComplete.thenApply(responseApplier);
    if (defaultValueOnExceptionSupplier != null) { //when we have default value the exception will not thrown
        returnedFuture = returnedFuture.exceptionally((ex) -> defaultValueOnExceptionSupplier.get());
    }

    return returnedFuture;
}

Relevant code from CompletableFuture:

final <S> boolean uniApply(CompletableFuture<S> a,
                           Function<? super S,? extends T> f,
                           UniApply<S,T> c) {
    Object r; Throwable x;
    if (a == null || (r = a.result) == null || f == null)
        return false;
    tryComplete: if (result == null) {
        if (r instanceof AltResult) {
            if ((x = ((AltResult)r).ex) != null) {
                completeThrowable(x, r);
                break tryComplete;
            }
            r = null;
        }
        try {
            if (c != null && !c.claim())
                return false;
            @SuppressWarnings("unchecked") S s = (S) r;
            completeValue(f.apply(s)); // This is CompletableFuture.java:616
        } catch (Throwable ex) {
            completeThrowable(ex);
        }
    }
    return true;
}

Here is how I invoke ConcurrentUtils.buildCompletableFuture:

ConcurrentUtils.buildCompletableFuture(asyncTask,
            ObjectWithTimestamp::getObject, s -> {},
            e -> logger.error("error message"), null, MoreExecutors.directExecutor())

The f in uniApply() is actually ObjectWithTimestamp::getObject

  • 6
    Turns out the NPE is not really in the uniApply but in the function I applied. The reason the JVM is not showing the applied function in the stack trace is because the applied function is a method reference. Once I changed the method reference into a lambda, the stack trace for this error showed the real NPE location. – Lior Halfon Aug 23 '22 at 09:13
  • 1
    @user207421 I don't think the linked question is relevant. The linked question is generally about `NPE`s, what they are and how to fix them. While this about one specific instance occurring within the JDK. The OP showed an effort on figuring out the root cause and got stuck at one point. – michid Aug 23 '22 at 09:14
  • 1
    @michid Ultimately all `NullPointerExceptions` come down to a single cause: a null reference was either dereferenced, or about to be derefenced by a piece of code that threw it in self-defence. It is about 9 orders of magnitude more likely that this is due to a `null` value supplied by the caller than a `null` value arising out of the JRE's internal operations. The OP's comment above, posted *before* your posted your comment, confirms this assertion. – user207421 Aug 23 '22 at 10:07
  • 2
    But I think this is still important to keep this post since the stack trace does not point you directly to the problem, and is very misleading. Surely it's not the same question as "What is a NullPointerException?". – Lior Halfon Aug 23 '22 at 11:34
  • 2
    BTW, https://stackoverflow.com/questions/46898164/java-8-method-reference-to-class-instance-method-npe is a bit more relevant than the current linked post "What is a NullPointerException" – Lior Halfon Aug 23 '22 at 12:15
  • @LiorHalfon I disagree. Ultimately the user supplied a null reference. He said so. It's all the same thing, and it is barking up the wrong tree to blame the framework or the JDK, as both you and the OP originally did. – user207421 Aug 24 '22 at 10:23
  • I never blamed the JRE for having a bug. I simply didn't understand why the stack trace looked like it did, and it didn't make sense. I spent a lot of time on it, asked many people and searched the web before I posted this question. All I'm saying is this post can help others and maybe save them some time. (Just don't want it to be deleted) – Lior Halfon Aug 25 '22 at 16:56

0 Answers0