Single responsibility.
Never ignore or cover up bugs.
I will point out that the function predictBallDestination has gone way outside its role by throwing an error due to a game state that should never happen.
Even if it is possible somehow for the ball to have no horizontal movement, detecting this state should be done at a much higher level.
Yes this function is where vx = 0 will matter, but then a divide by zero is the least of your problems.
Always attempt to recover the correct state, and reserve throwing errors only when there is absolutely no way to recover the valid state.
Ask yourself "What is setting vx == 0?" and fix the problem there! Throwing an error is just ignoring the underlying bug.
More constants
You code is doing a lot of repeated calculations. Most notable you are subtracting BALLRADIUS many times. Extend your list of constants to avoid the needless math.
For example to define a play field with inset
const WIDTH = canvas.width; // Whatever sets the size
const HEIGHT = canvas.height;
const INSET = 50;
const BALLRADIUS = 10;
const LEFT = INSET + BALLRADIUS;
const RIGHT = WIDTH - INSET - BALLRADIUS;
const TOP = INSET + BALLRADIUS;
const BOTTOM = HEIGHT - INSET - BALLRADIUS;
To many conditions
You are checking the ball velocity over and over, is ball going left or right, up or down. That can all be done in the math. The only check needed is which edge you want the y position at.
If you set the max slope of the ball's travel such that it does not hit the top and bottom edge more than say 2000 times you can simplify the final odd even check.
// using constants from above
const MAX_BOUNCES = 2000; // number of times ball hits top and bottom
function getYPos(x, y, vx, vy) {
const h = BOTTOM - TOP;
const edge = vx < 0 ? LEFT : RIGHT;
const hy = ((y + (edge - x) * vy / vx) - TOP + h * MAX_BOUNCES) % (h * 2);
return (hy < h ? hy : h - hy + h) + TOP;
}