A typical way to handle UART input goes a bit further than you say:
When data has been received the UART generates a HW interrupt and the interrupt handler reads the data from UART and places it in a FIFO buffer, often a circular buffer.
At the higher level when the OS wants to receive data it looks at the input buffer to see if there is any data there.
This mechanism provides another layer of asynchronicity and it means that the input data flow control need only block the remote transmitter when the receiver's input buffer is (nearly) full.
outputThe UART generates a HW interrupt when it is ready to transmit data. The interrupt handler will then look at the FIFO output buffer and place the first item in the queue in the UART's transmit register. Otherwise if there is no data waiting to be transmitted the interrupt status is cleared.
At the higher level when the OS wants to transmit data it places the item in the output buffer and ensures that the UART will generate a HW interrupt when it is ready to transmit, which may be straight away.
This means that the output data flow is only blocked when the output buffer is full.