4

Before async/await, when my code used callbacks, I was able to do three things: (1) call the callback with a result, (2) call the callback with an Error, or (3) not call the callback at all.

Case (3) was used in situations like this: say that you have a zoom button and a user can click it to render an image at a higher resolution, and this is an async process. If the user clicks the zoom button again, then the first render is no longer relevant, and can be canceled to let the new zoom level render run. I handled this by returning from inside the function without calling the callback, e.g.

if (process.wasCanceled()) {
  return;
}
// ...
callback(someResult);

With async/await, there are only two things that you can do: return or throw. Currently, I've been using throw to indicate that the operation was canceled, since returning can falsely indicate that upstream processes should keep running. But the problem with throwing is that all the upstream callers need to know that it's not really an error, per se, and so they may need to check the type of the error.

Another crazy idea I had was to create a promise that never returns. E.g. await never(), where the function is defined like this:

async function never () {
    return new Promise(function () {});
}

That is sort of the equivalent of not calling a callback.

But I don't know if that would just leak memory over and over.

Is there a better equivalent without the drawbacks I mentioned above?

Chris Middleton
  • 5,654
  • 5
  • 31
  • 68
  • 2
    I'd recommend using throw with a specific message, like "canceled". Yes, everything upstream would have to look for this... But isn't this what you want? – ControlAltDel Sep 21 '17 at 15:11
  • 2
    This is a really interesting question. I do believe `never()` is a very bad idea for the memory leak reason. @ControlAltDel it would seem to me like he wants a situation where the upstream *wouldn't* have to look, because in his first example, if `process.wasCanceled()`, the callback just doesn't run. – TKoL Sep 21 '17 at 15:12
  • As I think about the `await never()` more, I think it might be OK. These two questions (https://stackoverflow.com/questions/20068467/ and https://stackoverflow.com/questions/38286358) seem to indicate that the browser might garbage collect the promise created by the `never` function, since there is nothing in the promise's function that connects it to the rest of the heap. So perhaps it won't leak. – Chris Middleton Sep 21 '17 at 15:16
  • 2
    I don't see the problem with throwing a cancel error. All your upstream callers should propagate without you having to do anything, except for the topmost which needs to know that the op was canceled. – Stephen Cleary Sep 21 '17 at 15:19
  • @ControlAltDel I left out a crucial part of the architecture, which is that the upstream caller creates the Process object and passes it to the callee. So the only way that the callee can be canceled is if the caller (or one of its callers) initiated the cancel. So the caller is already OK with the decision to cancel - it just doesn't know. This does ignore however any in-between functions that might need to clean up processes, so you make a good point. – Chris Middleton Sep 21 '17 at 15:19

1 Answers1

1

If absolutely necessary, you can await a promise that never returns. This is the equivalent of not calling a callback.

async function never () {
    return new Promise(function () {});
}

async function op (process) {
    // ...
    if (process.wasCanceled()) await never();
    // ...
}

According to these answers, this will be garbage collected, because the returned promise is never used and there are no connections to the heap inside the promise's function argument.

Do never resolved promises cause memory leak?

Are JavaScript forever-pending promises bad?

However, this is most likely not what you want to do, since upstream callers may like to know that their operation has been canceled. If the operation was initiated by a user through the UI, then yes, canceling without telling the caller is probably OK, but if the operation was initiated programmatically and cancelled some other way, e.g. by the user, then the calling code might need to know that, so that it can try again, or clean up resources.

For this reason, the solution is to throw an error, of a specific class so that the caller can detect that the process was cancelled. E.g.

class ProcessCanceledError extends Error {
    ...
}

async function render (process) {
    while (...) {
        // do some rendering
        await delay(20);
        if (process.wasCanceled()) throw new ProcessCanceledError();
    }
}

var zoomProcess;

async function zoom () {
    let process = new Process();
    if (zoomProcess != null && !zoomProcess.isDone()) {
        zoomProcess.cancel();
    }
    try {
        await render();
    } catch (e) {
        // or you could do e.process === process
        if (e instanceof ProcessCanceledError &&
            process.wasCanceled() // make sure it was actually ours
        ) {
            // this assumes we are a top level function
            // otherwise, you would want to propagate the error to caller's caller
            return;
        }
        throw e;
    }
}
Chris Middleton
  • 5,654
  • 5
  • 31
  • 68