Note that if you do e.g. collect(intersect(1:2,4:5)) it returns a 0-element Array{...} too, so if you compare the collected results you get the same, i.e. collect(intersect(r1,r2)) == intersect(collect(r1),collect(r2)) for unitranges r1 and r2.
Effectively, something like 4:2 is an empty range since the step is +1 (not to be confused with a non-empty range like 4:-1:2). The advantage is that intersect of unitranges seems to always return unitranges which is important for type stability. Quite nice if you ask me! (And you might wanna take a look at the implementation - very simple and understandable)

Ok, that makes sense. I don’t know why, but for a moment I was thinking of 4:2 as 4:-1:2. The argument about type stability makes total sense. Thanks a lot.

I also noticed that intersect!() throws an error when applied to unit ranges:

julia> s = 1:5
1:5
julia> s = intersect(s, 4:5)
4:5
julia> s = 1:5
1:5
julia> intersect!(s, 4:5)
ERROR: setindex! not defined for UnitRange{Int64}
Stacktrace:
[1] error(::String, ::Type) at ./error.jl:42
[2] error_if_canonical_setindex(::IndexLinear, ::UnitRange{Int64}, ::Int64) at ./abstractarray.jl:1028
[3] setindex! at ./abstractarray.jl:1019 [inlined]
[4] filter!(::getfield(Base, Symbol("##83#84")){typeof(∉),typeof(push!),Set{Int64}}, ::UnitRange{Int64}) at ./array.jl:2339
[5] _shrink!(::Function, ::UnitRange{Int64}, ::Tuple{UnitRange{Int64}}) at ./array.jl:2384
[6] intersect!(::UnitRange{Int64}, ::UnitRange{Int64}) at ./array.jl:2389
[7] top-level scope at none:0

Yeah, that method may be too broadly typed; there are a lot of immutable concrete types <: AbstractVector. This is a case where trait-based dispatch would be useful.

I suppose that empty ranges could retain independent start and stop values but what would the advantage be? The current behavior allows the length of a range to always be computed as stop-start+1. It also normalizes empty ranges starting at a given index.