Через пару уроков начну подробно копать эту тему, с дебагом и исходниками. Пока же могу изложить мое текущее понимание.
coroutineScope изолирует созданные в нем корутины. Если какая-то из его корутин выдаст ошибку, то отменятся все корутины внутри coroutineScope. Но выше coroutineScope эта отмена не пойдет.
coroutineScope не принимает на вход Context. Т.е. он не сможет, например, сменить поток. Хотя корутины, созданные внутри него, смогут, если это необходимо.
withContext делает тоже самое, но позволяет указать новый Context, чтобы поток выполнения поменять.
Если сделать такой вызов - withContext(this.coroutineContext), то получится полный аналог вызова coroutineScope().
coroutineScope предполагается использовать для создания отдельного изолированного блока корутин, которые будут выполнятся в нужных им потоках. А при ошибке этот блок не будет отменять все, что выше его.
withContext же используется чтобы быстро что-то сделать в другом потоке (обновить UI, например) и вернуться обратно. В нем обычно простой код, без создания корутин.
Но, это не правило, разумеется. Случаи всякие бывают.