Rethrowing exceptions at place of function/object construction

59 Views Asked by At

In the case of functions, when exceptions may only throw at their place of execution (based on my lack of knowledge this is uncertain for me at the moment), sometimes the culprit may rely on the way the lambda was created, an example may be:

    // merge() will throw an exception if Function<S, Observable<T>> switchMap returns null.
    public<S> void mergeSwitch(Observable<S> source, Function<S, Observable<T>> switchMap) {
        final Mergeable<T> res = new Mergeable<>();
        final Consumer<T> observer = creator(res, this); // Secondary subscription

        setSource(
                source, // Main subscription
                new StateAwareObserver<S>() { // >>> implements Consumer<S>
                    Observable<T> switchRes = new Observable<>(); // Mediator

                    @Override
                    protected void onAdded() {
                        res.add(observer);
                    }

                    @Override
                    protected void onRemoved() {
                        res.remove(observer);
                    }

                    @Override
                    public void accept(S s) {
                         Observable<T> switchS = switchMap.apply(s);
                         if (switchRes != switchS) {
                             switchRes = switchS;
                             res.merge(switchS); //Where merge() throws if switchMap returns null
                         }
                    }
                }
        );
    }
}

I've tried rethrowing the exception from the accept() method but it never gets pushed(pulled?) beyond its point of consumption, (where the consumer accepts "s"):

        for (int i = observers.size() - 1; i >=0 ; i--) {
            observers.get(i).accept(processed); // << -- StateAwareObserver<S> is stored in the *list*
        }

-In this case I am using the word "beyond", but what I need is to throw the exception at its point of construction.-

In reality the real culprit may have been that the execution of the method: mergeSwitch(Observable<S> source, Function<S, Observable<T>> switchMap) which created the Consumer<>, may have left something important out.

Or as is my case, the method executing mergeSwitch():

    public<S> Forkable<S> forkSwitch(Function<T, Observable<S>> switchMap) {
        Forkable<S> res = new Forkable<>();
        res.mergeSwitch(this, switchMap);
        return res;
    }

In this case the issue was that the function was left with a null value.

public Holders.Forkable<List<Preset>> ledgerPresets = presetDao.forkSwitch(
        presetDao -> ledgerId.forkSwitch(
                aLong -> null
        )
);

So if we take a look at the Consumer level, the real culprit would be:

                    @Override
                    public void accept(S s) {
                        int sVersion = source.getVersion();
                        if (sVersion != oVersion) {
                            Observable3<T> switchS = switchMap.apply(s); //< -- HERE
                            if (switchRes != switchS) {
                                switchRes = switchS;
                                res.merge(switchS);
                            }
                            oVersion = sVersion;
                        }
                    }

In this particular scenario, things can get out of hand easily since, the log may not stop at its nearest point of consumption, but the error may go all the way back to the root node.

Things can get even more complicated since the merge() method, which is the origin of the exception, may have been used from 3 different methods.

The ideal scenario would be that the exception would be thrown here:

public Holders.Forkable<List<Preset>> ledgerPresets = presetDao.forkSwitch(
        presetDao -> ledgerId.forkSwitch(   // <--- HERE
                aLong -> null
        )
);

But I haven't found a way to manage this particular exception rethrowing case yet.

1

There are 1 best solutions below

0
On

EDIT:

In my previous answer I spent so much time trying to push the exception a stacktrace level, that I simply stopped trying to do it the easy way, you can ignore the craziness of the answer bellow the new one:

BEST ANSWER

    public<S> Forkable<S> forkSwitch(Function<T, Observable<S>> switchMap) {
        final IllegalStateException e = new IllegalStateException("switchMap must not be null");

        final Forkable<S> res = new Forkable<>();
        res.mergeSwitch(this,
                t -> {
                    Observable<S> prev = switchMap.apply(t);
                    if (prev == null) {
                        throw e;
                    }
                    return prev;
                }
        );
        return res;
    }

OLD ANSWER

I tried searching for an answer but did not came up with an ideal one.

My solution has been capturing an int storing the top stacktrace and then infering the class name via the lambda.

    public<S> Forkable2<S> forkSwitch(Function<T, Observable3<S>> switchMap) {
        int lineNumber = Thread.currentThread().getStackTrace()[3].getLineNumber();
        Forkable2<S> res = new Forkable2<>();
        res.mergeSwitch(this,
                t -> {
                    Observable3<S> prev = switchMap.apply(t);
                    if (prev == null) {
                        String lambdaS = switchMap.toString();
                        String prevLambdaS = lambdaS.substring(0, lambdaS.indexOf("$"));
                        throw new IllegalStateException("Switch map function must not return null!! at(" + prevLambdaS.substring(prevLambdaS.lastIndexOf(".") + 1) + ".java:"+ lineNumber + "), \n" +
                                "from: " + this);
                    }
                    return prev;
                }
        );
        return res;
    }

The reason why I find this solution to be lackluster is that the origin of the exception is actually not used, what we are really doing is intercepting the event before it actually happens, but imo this brings redundancy.

fortunately the exception is now more clear than before:

java.lang.IllegalStateException: Switch map function must not return null!! at(PresetViewModel.java:66),

I believe one could encapsulate the entire process in a neat little object that creates the stacktrace before entering a lambda, and then creates an exception on command when required inside the lambda itself.

public<T, S> void toThrow(Function<T, S> throwableLambda) {
    InferableThrow iT = new InferableThrow();
    subscribe(
        t -> {
            S returned = throwableLambda.apply(t);
            if (returned == null) {
                throw iT.createExc(Class<? extends Exception>, Function<>, String message);
            }
            return returned;
        }
    );
}

Here is a helper object:

public final class InferableException<E extends RuntimeException> implements Supplier<E> {
    private final Supplier<E> type;

    public static<E extends RuntimeException> InferableException<E> create(
            Function<String, E> type, Object lambdaObject, String message
    ) {
        return new InferableException<>(type, lambdaObject, message);
    }

    private InferableException(Function<String, E> type, Object lambdaObject, String message) {
        int lineNumber = Thread.currentThread().getStackTrace()[5].getLineNumber();
        String lambdaS = lambdaObject.toString();
        String prevLambdaS = lambdaS.substring(0, lambdaS.indexOf("$"));
        String className = prevLambdaS.substring(prevLambdaS.lastIndexOf(".") + 1);
        this.type = () -> type.apply(message +" at("+ className + ".java:" + lineNumber + ")");
    }

    @Override
    public E get() {
        return type.get();
    }
}

Usage:

    public<S> Forkable<S> forkSwitch(Function<T, Observable<S>> switchMap) {
        final Supplier<IllegalStateException> e = InferableException.create( //<<-- construction
                IllegalStateException::new,
                switchMap,
                "Switch map function must not return null!!"
        );
        Forkable<S> res = new Forkable<>();
        res.mergeSwitch(this,
                t -> {
                    Observable<S> prev = switchMap.apply(t);
                    if (prev == null) {
                        throw e.get();   //<<-- Throwing!!
                    }
                    return prev;
                }
        );
        return res;
    }