Usage
The Result type
Fundamentally, we have Result{T, E}. This type contains either a successful value of type T or an error of type E. For example, a function returning either a string or a 32-bit integer error code could return a Result{String, Int32}.
You can construct it like this:
julia> Result{String, Int}(Ok("Nothing went wrong"))
Result{String, Int64}(Ok("Nothing went wrong"))Thus, a Result{T, E} represents either a successful creation of a T or an error of type E.
Option
Option{T} is an alias for Result{T, Nothing}, and is easier to work with fully specifying the parameters of Results. Option is useful when the error state do not need to store any information besides the fact than an error occurred. Options can be conventiently created with two helper functions some and none:
julia> some(1) === Result{typeof(1), Nothing}(Ok(1))
true
julia> none(Int) === Result{Int, Nothing}(Err(nothing))
trueIf you want an abstractly parameterized Option, you can construct it directly like this:
julia> Option{Integer}(some(1))
Option{Integer}(some(1))ResultConstructor
Internally, Result{T, E} contains a field typed Union{Ok{T}, Err{E}}. The types Ok and Err are not supposed to be instantiated directly (and indeed, cannot be easily instantiated).
Calling Ok(x) or Err(x) instead creates an instance of the non-exported type ResultConstructor:
julia> Err(1)
ErrorTypes.ResultConstructor{Int64, Err}(1)
julia> Ok(1)
ErrorTypes.ResultConstructor{Int64, Ok}(1)The only purpose of ResultConstructor is to more easily create Results with the correct parameters, and to allow conversions of carefully selected types, read on to learn how. The user does not need to think much about ResultConstructor, but if ErrorTypes is abused, this type can show up in the stacktraces.
none by itself is a constant for ErrorTypes.ResultConstructor{Nothing, Err}(nothing) - we will come back to why this is particularly convenient.
Basic usage
Always typeassert any function that returns an error type. The whole point of ErrorTypes is to encode error states in return types, and be specific about these error states. While ErrorTypes will technically work fine without function annotations, it makes everything easier, and I highly recommend annotating return types:
Do this:
invert(x::Integer)::Option{Float64} = iszero(x) ? none : some(1/x)And not this:
invert(x::Integer) = iszero(x) ? none(Float64) : some(1/x)When annotating a function with a return type T, the return value gets converted at the end with an explicit convert(T, return_value).
In the function in this example, the function can return none, which was a generic instance of ResultConstructor. When that happens, none is automatically converted to the correct value, in this case none(Float64). Similarly, one can also use a typeassert to ease in the construction of Result return type:
function get_length(x)::Result{Int, Base.IteratorSize}
isz = Base.IteratorSize(x)
if isa(isz, Base.HasShape) || isa(isz, Base.HasLength)
return Ok(Int(length(x)))
else
return Err(isz)
end
endIn the above example example, Ok(Int(length(x)) returns a ResultConstructor{Int, Ok}, which can be converted to the target Result{Int, Base.IteratorSize}. Similarly, the Err(isz) creates a ResultConstructor{Base.IteratorSize, Err}, which can likewise be converted.
In most cases therefore, you never have to constuct an Option or Result directly. Instead, use a typeassert and return some(x) or none to return an Option, and return Ok(x) or Err(x) to return a Result.
Conversion rules
Error types can only convert to each other in certain circumstances. This is intentional, because type conversion is a major source of mistakes.
- A
Result{T, E}can be converted toResult{T2, E2}iffT <: T2andE <: E2, i.e. you can always convert to a "larger" Result type or to its own type. - A
ResultConstructor{T, Ok}can be converted toResult{T2, E}ifT <: T2. - A
ResultConstructor{E, Err}can be converted toResult{T, E2}ifE <: E2.
The first rule merely state that a Result can be converted to another Result if both the success parameter (Ok{T}) and the error parameter (Err{E}) error types are a supertype. It is intentionally NOT possible to e.g. convert a Result{Int, String} containing an Ok to a Result{Int, Int}, even if the Ok value contains an Int which is allowed in both of the Result types. The reason for this is that if it was allowed, whether or not conversions threw errors would depend on the value of an error type, not the type. This type-unstable behaviour would defeat idea behind this package, namely to present edge cases as types, not values.
The next two rules state that ResultConstructors have relaxed this requirement, and so a ResultConstructors constructed from an Ok or Err can be converted if only the Ok{T} or the Err{E} parameter, respectively, is a supertype, not necessarily both parameters. This is what enables use of Ok(x), Err(x) and none as return values when the function is annotated with return type.
There is one last type, ResultConstructor{T, Union{}}, which is even more flexible in how it converts. This is created by the @? macro, discussed next.
@?
If you make an entire codebase of functions returning Results, it can get bothersome to constantly check if function calls contain error values and propagate those error values to their callers. To make this process easier, use the macro @?, which automatically propagates any error values. If this is applied to some expression x evaluating to a Result containing a success value (i.e. Ok{T}), the macro will evaluate to the inner wrapped value:
julia> @? Result{String, Int}(Ok("foo"))
"foo"However, if x evaluates to an error value Err{E}, the macro creates a ResultConstructor{E, Union{}}, let's call it y, and evaluates to return y. In this manner, the macro means "unwrap the value if possible, and else immediately return it to the outer function". ResultConstructor{E, Union{}} are even more flexible in what they can be converted to: They can convert to any Option type, or any Result{T, E2} where E <: E2. This allows you to propagate errors from functions returning Result to those returning Option.
Let's see it in action. Suppose you want to implement a safe version of the harmonic mean function, which in turn uses a safe version of div:
safe_div(a::Integer, b::Real)::Option{Float64} = iszero(b) ? none : some(a/b)
function harmonic_mean(v::AbstractArray{<:Integer})::Option{Float64}
sm = 0.0
for i in v
invi = safe_div(1, i)
is_error(invi) && return none
sm += unwrap(invi)
end
res = safe_div(length(v), sm)
is_error(res) && return none
return some(unwrap(res))
endIn this function, we constantly have to check whether safe_div returned the error value, and return that from the outer function in that case. That can be more concisely written as:
function harmonic_mean(v::AbstractArray{<:Integer})::Option{Float64}
sm = 0.0
for i in v
sm += @? safe_div(1, i)
end
some(@? safe_div(length(v), sm))
endIn case any of the calls to safe_div yields a none(Float64), the @? macro evaluates to code equivalent to return ResultConstructor{Nothing, Union{}}(nothing). This value is then converted by the typeassert in the outer function to none(Float64)
When to use an error type vs throw an error
The error handling mechanism provided by ErrorTypes is a distinct method from throwing and catching errors. None is superior to the other in all circumstances.
The handling provided by ErrorTypes is faster, safer, and more explicit. For most functions, you can use ErrorTypes. However, you can't only rely on it. Imagine a function A returning Option{T1}. A is called from function B, which can itself fail and returns an Option{T2}. However, now there are two distinct error states: Failure in A and failure in B. So what should B return? Result{T2, Enum{E1, E2}}, for some Enum type? But then, what about functions calling B? Where does it end?
In general, it's un-idiomatic to "accumulate" error states like this. You should handle an error state when it appears, and usually not return it far back the call chain.
More importantly, you should distinguish between recoverable and unrecoverable error states. The unrecoverable are unexpected, and reveals that the program went wrong somehow. If the program went somewhere it shouldn't be, it's best to abort the program and show the stack trace, so you can debug it - here, an ordinary exception is better. If the errors are known to be possible beforehand, using ErrorTypes is better. For example, a program may use exceptions when encountering errors when parsing "internal" machine-generated files, which are supposed to be of a certain format, and use error types when parsing user input, which must always be expected to be possibly fallible.
Because error types are so easily converted to exceptions (using unwrap and expect), internal library functions should preferably use error types.
Reference
ErrorTypes.none — ConstantnoneSingleton instance of ResultConstructor{Nothing, Err}(nothing). This value is useful because it can be converted to any Option{T}, giving the error value.
See also: Option
Examples
julia> f(x)::Option{Float64} = iszero(x) ? none : 1 / x;
julia> f(0)
none(Float64)
julia> struct MaybeInt32 x::Option{Int32} end;
julia> MaybeInt32(none)
MaybeInt32(none(Int32))ErrorTypes.none — Methodnone(::Type)::Option{T}Construct the error value of Option{T}.
See also: Option
Examples
julia> none(String) === Option{String}(Err(nothing))
true
julia> none(Char) isa Option{Char}
true
julia> is_error(none(Char))
true
julia> convert(Option{Vector}, none) === none(Vector)
trueErrorTypes.Err — TypeErrThe error state of a Result{O, E}, carrying an object of type E. For convenience, Err(x) creates a dummy value that can be converted to the appropriate Result type.
For a more detailed description, see: Ok
ErrorTypes.Ok — TypeOk{T}The success state of a Result{T, E}, carrying an object of type T. For convenience, Ok(x) creates a dummy value that can be converted to the appropriate Result type.
Instances of Ok and its mirror image Err cannot be directly constructed.
See also: Err
julia> function reciprocal(x::Int)::Result{Float64, String}
iszero(x) && return Err("Division by zero")
Ok(1 / x)
end;
julia> reciprocal(4)
Result{Float64, String}(Ok(0.25))
julia> reciprocal(0)
Result{Float64, String}(Err("Division by zero"))ErrorTypes.Option — TypeOption{T}Alias for Result{T, Nothing}. Useful when the error type of a Result need not store any information. Construct value instances with some(x) and error instances with none(::Type).
See also: Result
Examples
julia> some(4) isa Option{Int}
true
julia> is_error(some(4))
false
julia> none(String) isa Option{String} && is_error(none(String))
trueErrorTypes.Result — TypeResult{O, E}A sum type of either Ok{O} or Err{E}. Used as return value of functions that can error with an informative error object of type E.
Results are normally constructed implicitly, through converting ResultConstructor using Ok or Err, such as in:
julia> x::Result{Int, String} = Err("Oh my!");
julia> x
Result{Int64, String}(Err("Oh my!"))Examples
julia> Result{UInt8, UInt32}(Ok(0x03)) # manual constructor
Result{UInt8, UInt32}(Ok(0x03))
julia> make_err()::Result{Vector{Int}, String} = Err("error!");
julia> make_err()
Result{Vector{Int64}, String}(Err("error!"))
julia> is_error(make_err())
trueErrorTypes.ResultConstructor — TypeErr(x::T)::ResultConstructor{T, Err}
Ok(x::T)::ResultConstrutor{T, Ok}
none::ResultConstructor{Nothing, Err}
(@? x::Result{T,E}(Err{E}))::ResultConstructor{T, Union{}}Instances of ResultConstructor are temporary values, which are constructed only to be immediately converted to Result. Proper use of ErrorTypes.jl should not result in ResultConstructors leaking out of functions.
The constructors Ok and Err return ResultConstructor, and the constant none is the ResultConstructor for Option.
Typical use of ResultConstructor is to construct it immediately before returning it. If the function's return type is annotated to Result, the value will be converted.
Examples:
julia> sat_sub(x::UInt8)::Result{UInt8, String} = iszero(x) ? Err("Overflow") : Ok(x - 0x01);
julia> sat_sub(0x00)
Result{UInt8, String}(Err("Overflow"))
julia> sat_sub(0x01)
Result{UInt8, String}(Ok(0x00))
julia> sat_sub(x::UInt8)::Option{UInt8} = iszero(x) ? none : Ok(x - 0x01);
julia> sat_sub(0x00)
none(UInt8)
julia> sat_sub(0x01)
some(0x00)ErrorTypes.and_then — Methodand_then(f, ::Type{T}, x::Result{O, E})::Result{T, E}If is a result value, return Result{T, E}(Ok(f(unwrap(x)))), else return the error value. Always returns a Result{T, E}.
WARNING If f(unwrap(x)) is not a T, this functions throws an error.
Examples
julia> and_then(join, String, some(["ab", "cd"]))
some("abcd")
julia> and_then(i -> Int32(ncodeunits(join(i))), Int32, none(Vector{String}))
none(Int32)ErrorTypes.base — Methodbase(x::Option{T})Convert an Option{T} to a Union{Some{T}, Nothing}.
See also: Option
Examples
julia> sub_nonneg(x::Int)::Option{Int} = x < 1 ? none : some(x - 1);
julia> base(sub_nonneg(-3)) === nothing
true
julia> base(sub_nonneg(2))
Some(1)ErrorTypes.expect — Methodexpect(x::Result, s::AbstractString)If x is of the associated error type, error with message s. Else, return the contained result type.
See also: expect_error, unwrap
Examples
julia> expect(some('x'), "cannot be none") === 'x'
true
julia> expect(Result{Int, String}(Ok(19)), "Expected an integer")
19ErrorTypes.expect_error — Methodexpect_error(x::Result, s::AbstractString)If x contains an Err, return the content of the Err. Else, throw an error with message s.
See also: unwrap_error, expect
Examples
julia> expect_error(none(Int), "expected none") === nothing
true
julia> expect_error(Result{Vector, String}(Err("Mistake!")), "must be error")
"Mistake!"
julia> expect_error(some(3), "must be none")
ERROR: must be none
[...]ErrorTypes.flatten — Methodflatten(x::Option{Option{T}})Convert an Option{Option{T}} to an Option{T}.
Examples
julia> flatten(some(some("x")))
some("x")
julia> flatten(some(none(Float32)))
none(Float32)ErrorTypes.is_error — Methodis_error(x::Result)Check if x contains an error value.
Examples
julia> is_error(none(Int)), is_error(some(5))
(true, false)ErrorTypes.is_ok_and — Methodis_ok_and(f, x::Result)::BoolCheck if x is a result value, and f(unwrap(x)). f(unwrap(x)) must return a Bool.
Examples
julia> is_ok_and(isodd, none(Int))
false
julia> is_ok_and(isodd, some(2))
false
julia> is_ok_and(isodd, Result{Int, String}(Ok(9)))
true
julia> is_ok_and(ncodeunits, some("Success!"))
ERROR: TypeError: non-boolean (Int64) used in boolean contextErrorTypes.iter — Methoditer(x::Option)::OptionIteratorProduce an iterator over x, which yields the result value of x if x is some, or an empty iterator if it is none.
Examples
julia> first(iter(some(19)))
19
julia> collect(iter(some("some string")))
1-element Vector{String}:
"some string"
julia> isempty(iter(none(Dict)))
true
julia> collect(iter(none(Char)))
Char[]ErrorTypes.map_or — Methodmap_or(f, x::Result, v)If x is a result value, return f(unwrap(x)). Else, return v.
Examples
julia> map_or(isodd, some(9), nothing)
true
julia> map_or(isodd, none(Int), nothing) === nothing
true
julia> map_or(ncodeunits, none(String), 0)
0ErrorTypes.ok — Methodok(x::Result{T})::Option{T}Construct an Option from a Result, such that the Ok variant becomes a some, and the Err variant becomes a none(T), discarding the error value if present.
Examples
julia> ok(Result{Int32, String}(Err("Some error message")))
none(Int32)
julia> ok(Result{String, Dict}(Ok("Success!")))
some("Success!")
julia> ok(some(5))
some(5)ErrorTypes.unwrap — Methodunwrap(x::Result)If x is of the associated error type, throw an error. Else, return the contained result type.
See also: unwrap_error, @unwrap_or
Examples
julia> unwrap(some(Any[]))
Any[]
julia> unwrap(none(Int))
ERROR: unwrap on unexpected type
[...]
julia> unwrap(Result{String, Int32}(Ok("Lin Wei")))
"Lin Wei"ErrorTypes.unwrap_error — Methodunwrap_error(x::Result)If x contains an Err, return the content of the Err. Else, throw an error.
See also: unwrap, expect_error
Examples
julia> unwrap_error(some(3))
ERROR: unwrap on unexpected type
[...]
julia> unwrap_error(none(String)) === nothing
true
julia> unwrap_error(Result{Int, String}(Err("some error")))
"some error"ErrorTypes.unwrap_error_or — Methodunwrap_error_or(x::Result, v)Like unwrap_or, but unwraps an error.
See also: unwrap_or
Examples
julia> unwrap_error_or(Result{Int, String}(Err("abc")), 19)
"abc"
julia> unwrap_error_or(none(String), "error") === nothing
true
julia> unwrap_error_or(some([1, 2, 3]), Int32[])
Int32[]ErrorTypes.unwrap_error_or_else — Methodunwrap_error_or(f, x::ResultReturns the wrapped error value if x is an error, else return f(unwrap(x)).
See also: @unwrap_error_or, unwrap_or_else
Examples
julia> unwrap_error_or_else(ncodeunits, some("abc"))
3
julia> unwrap_error_or_else(ncodeunits, none(String)) === nothing
true
julia> unwrap_error_or_else(n -> n + 1, Result{Int, String}(Err("abc")))
"abc"ErrorTypes.unwrap_or — Methodunwrap_or(x::Result, v)If x is an error value, return v. Else, unwrap x and return its content.
See also: unwrap, @unwrap_or
Examples:
julia> unwrap_or(some(5), 9)
5
julia> unwrap_or(none(Float32), "something else")
"something else"
julia> unwrap_or(Result{Int8, Vector}(Err([])), 0x01)
0x01ErrorTypes.unwrap_or_else — Methodunwrap_or_else(f, x::Result)If x is an error value, return f(unwrap_error(x)). Else, unwrap x and return its content.
See also: unwrap_error_or_else, unwrap_or
Examples
julia> unwrap_or_else(isnothing, some(3))
3
julia> unwrap_or_else(println, none(Int))
nothing
julia> unwrap_or_else(ncodeunits, Result{Int, String}(Err("my_error")))
8ErrorTypes.@? — Macro@?(expr)Propagate a Result with Err value to the outer function.
Evaluate expr, which should return a Result. If it contains an Ok value x, evaluate to the unwrapped value x. Else, evaluates to return Err(x).
Examples
julia> (f(x::Option{T})::Option{T}) where T = Ok(@?(x) + one(T));
julia> f(some(1.0)), f(none(Int))
(some(2.0), none(Int64))ErrorTypes.@unwrap_error_or — Macro@unwrap_error_or(expr, exec)Evaluate expr to a Result. If expr is a result value, evaluate exec and return that. Else, return the wrapped error value in expr.
See also: @unwrap_or ```
ErrorTypes.@unwrap_or — Macro@unwrap_or(expr, exec)Evaluate expr to a Result. If expr is a error value, evaluate exec and return that. Else, return the wrapped value in expr.
See also: @unwrap_error_or
Examples
julia> safe_inv(x)::Option{Float64} = iszero(x) ? none : Ok(1/x);
julia> function skip_inv_sum(it)
sum = 0.0
for i in it
sum += @unwrap_or safe_inv(i) continue
end
sum
end;
julia> skip_inv_sum([2,1,0,1,2])
3.0