It depends on what the callback is for.
If it's just to report the asynchronous completion of the function's work, then there's no reason for it — async functions do that with the promise they return.
But if it's for a different reason, it can be valid. For instance, an async function that copies data from point A to point B might accept an optional callback that transforms a record of that data in some way: The async function reads a record from point A, passes it through the callback, and rites the result to point B; the promise the function returns is for the entire operation. (Not the best example in the world, you might write that as a pair of async generators instead, but...)
I had reason recently to write this odd-looking duck:
function nextFrame(cb) {
return new Promise(resolve => {
requestAnimationFrame(() => {
cb();
resolve();
});
});
}
...so that I could do this in an async function:
await nextFrame(() => {/*...updates for frame here...*/});
I can't just make it
await nextFrame();
// ...updates for frame here...
for a couple of reasons (because the processing model means my subsequent code wouldn't be run before the frame, and because I wouldn't want all of the work following it in my async function until the next await to be just prior to the frame being drawn).
Additionally, @VLAZ makes a very good point in their comment, if an API previously supported callbacks for completion where the function didn't previously return anything, it can be retrofitted to return a promise but still accept a callback. One of the MongoDB client libs did that IIRC.