A Julia equivalent to R's stop()?

Is there an equivalent in Julia to the stop() function in R? The nice thing about stop() is that it can be run from any scope (e.g. a function within a function) and it will throw an error and terminate the function that the user called.

I see that throw() in Julia does something similar, but I’d like to avoid printing a stack trace because it is not necessary for the info I’m trying to convey to users (e.g. two arrays provided as arguments are not the same size, but they need to be)

For now, I’m doing things like:

there_is_an_error = check_for_the_error(arg1, arg2) # returns a Bool
there_is_an_error && (@error "error message"; return)

I’d like to be able to have everything contained inside of check_for_the_error() to make the code more tidy.

Also open to suggestions for different approaches entirely.

Thanks!

1 Like

@assert is generally what I would use for that, but it does print a stacktrace. It is a bit of a shorthand for what you are already doing.

julia> function gg(x,y)
       @assert x != y
       x+y
       end
gg (generic function with 1 method)

julia> gg(1,1)
ERROR: AssertionError: x != y
Stacktrace:
 [1] gg(::Int64, ::Int64) at ./REPL[49]:2
 [2] top-level scope at REPL[50]:1
1 Like

Thanks! Is there any way to suppress the stacktrace?

Ideally, the output would just look like this (no stacktrace, just a link to the line of code that causes it for developers’ reference):

┌ Error: A detailed error message
└ @ Main untitled-8dbe96a72552bcb86ffb481d3a092170:2

So that I can tell my users what is wrong (concisely) so they can fix it and try again. The user base I’m working with consists mostly of non-programmers, so I’m hoping to make things as straightforward and simple as possible.

Not that I know of, but I’ve never looked into suppressing it.

You could probably do something like:

struct Stop{T}
    answer::T
end

function stop(answer)
    throw(Stop(answer))
end

function start()
    stop(42)
end

try
    start()
    @info "No answer."
catch ex
    if isa(ex, Stop)
        @info "Exit with: $(ex.answer)"
    else
        rethrow(ex)
    end
end

This assumes that you are not already catching exceptions in the stack. If you are then you probably want to throw a “Stop” object that the other exception handlers can rethrow.

This is also not the approved way of unwinding the stack, throw is meant for errors not a normal exit.

3 Likes

I think it may be the perfect solution for this use case.

Note also that R’s stop() is something like this, except that R, by default, does not print a stacktrace — the user can request one with traceback(). I think that the idea is that error messages themselves convey enough information, which I do not think is true for any nontrivial computation.

1 Like

Agreed @Tamas_Papp – in my case, it’ll something like an input matrix having negative values when it shouldn’t, or matrix 1 not having the same dims as matrix 2, so really simple stuff.

Would I need to put that try/catch in the top-level function whenever I want to check for an error? I tried playing around with putting it inside a function within a function and wasn’t able to figure it out.

I’m a little surprised this is so hard. I would have assumed that we’d have facilities for suppressing stacktraces.

One (non-ideal) approach to this would be to create a custom REPL mode where stack-traces may be suppressed. You can do this with my package ReplMaker.jl. Here’s what that might look like:

using ReplMaker
using REPL: LineEdit

struct QuietError{T}
    msg::T
end

globalize(ex) = ex
function globalize(ex::Expr)
    if ex.head == :block
        ex.args = globalize.(ex.args)
        ex
    elseif ex.head == :(=) || ex.head == :function
        :(global $ex)
    else
        ex
    end
end

function suppressable_stacktrace_parser(s)
    ex = globalize(Meta.parse(s))
    quote
        try
            $ex
        catch e
            if e isa QuietError
                @info "Exit with $e"
            else
                rethrow(e)
            end
        end
    end
end

iscomplete(x) = true
function iscomplete(ex::Expr)
    if ex.head == :incomplete
        false
    else
        true
    end
end

function valid_julia(s)
    input = String(take!(copy(LineEdit.buffer(s))))
    iscomplete(Meta.parse(input))
end

Now we can do

julia> initrepl(suppressable_stacktrace_parser,
                prompt_text="quiet-julia> ",
                prompt_color = :blue,
                start_key=')',
                mode_name="Suppressable-Error-Mode",
                valid_input_checker=valid_julia);
REPL mode Suppressable-Error-Mode initialized. Press ) to enter and backspace to exit.

julia> )

quiet-julia> f(x) = g(x + 1)
f (generic function with 1 method)

quiet-julia> g(x) = h(x/2)
g (generic function with 1 method)

quiet-julia> function h(x)
                 if x < 0
                     throw(QuietError("x must be positive, got $x"))
                 end
                 x
             end
h (generic function with 1 method)

quiet-julia> h(-5)
[ Info: Exit with QuietError{String}("x must be positive, got -5")

Yeah, I found the potential workarounds to be too tricky to work with, so for now, I decided I would proceed with an approach similar to what I outlined in the origin post. I simplified it a bit so that check_for_the_error(arg1, arg2) will also throw the error message if needed and return a Bool specifying whether the error was triggered. So now I have this inside my top-level function:

check_for_the_error(arg1, arg2) && return

Not sure if this is the most ideal or elegant approach – but it was the cleanest I could get it, and works for my purposes.

1 Like

Maybe just make it a macro instead if you don’t like the && return?

1 Like

If you want to do something else than unwind and show the stack (eg provide an error message to the user), yes, you need a try / catch.

It can be in any caller, does not need to be top level.

That said, I don’t see why you are trying to avoid printing the stack trace. It is customary to do so in Julia (to the extent that you have to work hard to avoid it), and it aids debugging by providing some context for the error. This is important for generic code where methods can show up in a lot of different contexts.

I hear you. My package is essentially one user-facing function. Users have to provide file paths to spatial data as arguments, and I want to throw an error for my users (who are not programmers) if the data layers do not align spatially, so they can go into a GIS program and fix them. I worry that anything else may just add to potential confusion.

struct NoBackTraceException
    exc::Exception
end

function Base.showerror(io::IO, ex::NoBackTraceException, bt; backtrace=true)
    Base.with_output_color(get(io, :color, false) ? Base.error_color() : :nothing, io) do io
        showerror(io, ex.exc)
    end
    # look ma, no backtrace
end

f() = g()
g() = h()
h() = throw(NoBackTraceException(ErrorException("foo")))
julia> f()
ERROR: foo

julia>

Not really a good idea :man_shrugging:

5 Likes

It is hard to be specific without seeing some actual code, but something like

function do_everything(path)
    data = load_data(path)
    check_data_consistency(data) # errors can come from here
    ...
end

would provide a relatively flat stacktrace where you could include meaningful error messages about what went wrong. Incidentally, I find

very useful for this purpose.

1 Like

Thanks! I’ll check out that ArgCheck package.