Usage

The Result type

Fundamentally, we have the two types Ok{T} and Err{E}, each just being wrapper types of a T or E, respectively. These represent 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}.

Ok and Err are not supposed to be instantiated directly (and indeed, cannot be easily instantiated). Instead, they only exist as fields of a Result{T, E} type, which contains a field typed Union{Ok{T}, Err{E}}. 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

Behind the scenes, calling Err(x) or Ok(x) 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 create Results with the correct parameters, and to allow conversions of carefully selected types. 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)

Note in the first example that none can be returned, which was a generic instance of ResultConstructor. One purpose of ResultConstructor is that annotating the return type will cause this generic none to automatically convert to Option{Float64}. Similarly, one can also use a typeassert to ease in the construction of Result:

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 this 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.

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.

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_none(invi) && return none
        sm += unwrap(invi)
    end
    res = safe_div(length(v), sm)
    is_none(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

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.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.

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.

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.

ErrorTypes.and_thenMethod
and_then(f, ::Type{T}, x::Result{O, E})

If is a result value, apply f to 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.

ErrorTypes.baseMethod
base(x::Option{T})

Convert an Option{T} to a Union{Some{T}, Nothing}.

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.

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.

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(Int)))
none(Int)
ErrorTypes.map_orMethod
map_or(f, x::Result, v)

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

ErrorTypes.unwrapMethod
unwrap(x::Result)

If x is of the associated error type, throw an error. Else, return the contained result type.

ErrorTypes.unwrap_errorMethod
unwrap_error(x::Result)

If x contains an Err, return the content of the Err. Else, throw an error.

ErrorTypes.unwrap_orMethod
unwrap_or(x::Result, v)

If x is an error value, return v. Else, unwrap x and return its content.

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

Example

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_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.

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