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))
true

If 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
end

In 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 to Result{T2, E2} iff T <: T2 and E <: E2, i.e. you can always convert to a "larger" Result type or to its own type.
  • A ResultConstructor{T, Ok} can be converted to Result{T2, E} if T <: T2.
  • A ResultConstructor{E, Err} can be converted to Result{T, E2} if E <: 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))
end

In 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))
end

In 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.noneConstant
none

Singleton 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))
source
ErrorTypes.noneMethod
none(::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)
true
source
ErrorTypes.ErrType
Err

The 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

source
ErrorTypes.OkType
Ok{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"))
source
ErrorTypes.OptionType
Option{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))
true
source
ErrorTypes.ResultType
Result{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!"))

See also: Option, Ok, Err

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())
true
source
ErrorTypes.ResultConstructorType
Err(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)
source
ErrorTypes.and_thenMethod
and_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)
source
ErrorTypes.baseMethod
base(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)
source
ErrorTypes.expectMethod
expect(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")
19
source
ErrorTypes.expect_errorMethod
expect_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
[...]
source
ErrorTypes.flattenMethod
flatten(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)
source
ErrorTypes.is_ok_andMethod
is_ok_and(f, x::Result)::Bool

Check 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 context
source
ErrorTypes.iterMethod
iter(x::Option)::OptionIterator

Produce 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[]
source
ErrorTypes.map_orMethod
map_or(f, x::Result, v)

If x is a result value, return f(unwrap(x)). Else, return v.

See also: unwrap_or, and_then

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)
0
source
ErrorTypes.okMethod
ok(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)
source
ErrorTypes.unwrapMethod
unwrap(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"
source
ErrorTypes.unwrap_errorMethod
unwrap_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"
source
ErrorTypes.unwrap_error_orMethod
unwrap_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[]
source
ErrorTypes.unwrap_error_or_elseMethod
unwrap_error_or(f, x::Result

Returns 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"
source
ErrorTypes.unwrap_orMethod
unwrap_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)
0x01
source
ErrorTypes.unwrap_or_elseMethod
unwrap_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")))
8
source
ErrorTypes.@?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))
source
ErrorTypes.@unwrap_error_orMacro
@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 ```

source
ErrorTypes.@unwrap_orMacro
@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
source