Best practices for throw(...)?

I’ve read and reread the documentation for the exception term for throw/1, and I’m still confused.

For one thing, in error(..., context(Location,Message), the backtrace doesn’t seem to fill in Location:

?- listing(type_check:control_type_error).
control_type_error(Error, Goal, Context) :-
    throw(error(Error, context(Goal, Context))).

?- type_check:control_type_error(failure, 1=2, qqsv).
ERROR: Unknown error term: failure (qqsv)
ERROR: In:
ERROR:   [11] throw(error(failure,context(...,qqsv)))
ERROR:    [9] toplevel_call(user:type_check: ...) at /usr/lib/swi-prolog/boot/toplevel.pl:1113
ERROR: 
ERROR: Note: some frames are missing due to last-call optimization.
ERROR: Re-run your program in debug mode (:- debug.) to get more detail.

If I want to define a predicate that throws an exception on failure, is this the best way?

must_once(Goal) :-
    (   call(Goal)
    ->  true
    ;   throw(error(must_once_failed, context(_,goal:Goal)))
    ).

Or is this better?

must_once_msg(Goal, Msg, MsgArgs) :-
    (   call(Goal)
    ->  true
    ;   functor(Goal, Pred, Arity),
        format(string(MsgStr), Msg, MsgArgs),
        throw(error(must_once_failed(Goal),
                    context(Pred/Arity, MsgStr)))
    ).

Or something else?

1 Like

Well, the Location is the stack. As it says, the interesting frame is missing due to last call optimization. Handing your own name/arity will tell you the predicate, but not the stack. IMO that is a lot more typing, making your code a lot bulkier and harder to read while a partial stack is often still more useful than just a predicate without any context.

Last call optimization is necessary, but has its disadvantages when it comes to debugging. Prolog’s backtracking time machine is a HUGE advantage over other languages though :slight_smile:

So, is something like this the preferred form? (which is also what type:check_control_type_error/3 uses):

throw(error(must_once_failed, context(_,goal:Goal)))

I’d go for

error(must_once_failed(Goal), _).

I.d., leave the second argument simply unbound. Especially for your own errors you can capture enough context to generate a sensible message. Note that SWI-Prolog handles exceptions of the shape error(Formal, ImplDependent) special in the sense that the debugger wants to do something sensible with them. Other exception terms are left alone by the debugger and should be used for scenarios where you throw and intend to capture the exception as a normal part of the execution.

ISO had to split into Formal and ImplDependent as one system may be able to provide more context than another. So, ISO is limited in what it can demand to go into the Formal part of an error. If you define your own error you do not have that limitation.

If you want to stay in the ISO style, the formal part is *_error(Arg, …). A possible choice could be determinism_error(Expected, Found, Goal), e.g.

determinism_error(det, fail, Goal).

That said, IMHO far too much brain power of the Prolog community was wasted on this stuff :frowning: . For SWI-Prolog though, dealing with the error/2 vs other exceptions and typically leaving the second argument unbound ensures your code cooperates best with the development environment.

3 Likes