In many positions, different moves are functionally the same, and an engine should return the same evaluation. For example, in a KR vs. KR endgame, (almost) every move that does not lose the rook should give exactly the same evaluation: 0.00. How does an engine choose between them then?
The answer is simple for traditional engines like Stockfish and the like: if multiple moves have the same evaluation, they pick whichever move was searched first.
All strong alpha-beta engines employ move-ordering heuristics which guess the moves that are likely to be strongest in a position, before actually searching those moves. This is advantageous because the best case for alpha-beta search (especially for PVS) occurs when the strongest move is searched first, allowing beta cutoffs to occur as soon as possible and also avoiding repeated searches.
So when two moves are found to have the same evaluation, the tie is broken by move ordering heuristics.
Evaluation granularity also plays a role. Stockfish evaluates positions to the nearest 1/256th of a pawn, but only reports evaluations in 1/100th of a pawn, so there could be internal differentiation which is not visible to a user. MCTS engines like Lc0 make decisions based on the number of nodes searched in the subtrees of each move. This information wouldn't make sense to report as centipawns, so a conversion function is used. If many nodes have been searched, multiple moves may map to the same centipawn evaluation even if they can be differentiated internally.