3

The application this question is about is basically a transpiler which contains a lot of logic. The transpiler is written in C++ (which should not be much of relevance for this question), and it transpiles a special language (which we developed) into a different language.

For error handling (errors originating in invalid input as well as internal errors of the transpiler), I use exceptions. To keep the transpiler's logic as simple as possible, the input is validated very late, i.e. in most cases no separate validation is taking place. This means, the processing logic throws exceptions. Some utility functions are used globally thoughout the code base, and those can throw exceptions, too.

That means, exceptions are potentially thrown in a utility function, in which we do not know where the input comes from. That is a problem, because -- similar to how your compiler tells you in which line of which file the parse error occurred -- I'd like to tell the user of the transpiler where the error originated. To keep things simple, that does not necessarily have to be some "stack" or "chain" of contexts. But the exceptions should carry at least some contextual information, which part of the input is invalid and why, such that the relevant part of the input can be corrected and/or an IDE can navigate to the error's source.

For the exceptions, I currently use one single class Error which carries all information as members, and static functions are used to create them depending on what error occurred, for example:

throw Error::internalError("This should not have happened...");

or a more interesting case (pseudo code, this is not C++):

var x = ...;
if (!x.isInteger())
    throw Error::notAnInteger(x);

Now I'd like to augment that exception (the Error object) with a "context / location specification". For example, in my transpiled language, I have the concept of variables, which have some predefined value. Let's consider a variable which has an internal ID stored as an integer, and a value which we can retrieve. Now we want to make sure that the value is an integer (again, pseudo code):

var variableID = ...;
var value = getValueOfVariable(variableID);
if (!value.isInteger())
    throw Error::notAnInteger(value);

Now consider the case where variableID = 42, value = "foo". When the exception will be thrown and presented to the user, she only knows that "foo" is not an integer, but she is not being told that this value comes from the variable 42.

That's why I want to tell the Error somehow that we are trying to parse the variable whose ID is variableID.

I'd like to present you three ideas which I came up with, but can't decide which one is better style.


The first idea is to add additional static functions, each for a type of such a context. In the example above, instead of

    throw Error::notAnInteger(value);

I then do

    throw Error::variableNotAnInteger(value, variableID);

and within that function I store the information that the error source was "variable with ID variableID". However, with more and more such types of contexts (it is not only parsing the value of a variable which can lead to "not an integer" errors), the number of functions grows rapidly.


A second idea was to introduce a Location class, which has the purpose of describing the error source / context, and pass that along with the other parameters to these static functions:

    throw Error::notAnInteger(value, Location::variable(variableID));

However, that means that all utility functions (for example a function which has only the purpose to parse a value, independently of where the value comes from), would need to be augmented with the contextual Location object, in order to appropriately construct the Error object to be thrown. That however makes the code base kind of ugly, in particular for nested and further nested logic.


A third idea is to add the location after the exception was thrown. The utility function throws an exception, and the caller then catches the exception, adds the appropriate location, and then throws the augmented exception again. This has the drawback that when I forget somewhere in the code base to add such a location, the error does not carry one. (In contrast, the second idea makes sure that a location object is being passed.)

This would look like that (again, pseudo code):

function parseValue(value, type) {
    ...
    if (type == "integer") {
        if (!value.isInteger()) {
            // Throw an exception without a location / context information:
            throw Error::notAnInteger(value);
        }
        return value.toInteger();
    }
    ...
}

//-----------------------------------------

var variableID = ...;
var value = getValueOfVariable(variableID);
var type = getTypeOfVariable(variableID);
try {
    var parsedValue = parseValue(value, type);
} catch (Error e) {
    // Add the location of the error, and rethrow:
    e.setLocation(Location::variable(variableID));
    throw e;
}

Which of the above methods can be considered a good style? Is there even a fourth option I should consider? How is this typically done in trans-/compilers?

1
  • Is the transpiler mainly used by programmers like your team members? Is it mainly used under the supervision of a human (programmer)? Is it used with the debugger attached? Considering the errors you have seen so far, are they always reproducible when the same inputs are applied? These will influence your best error-handling strategy, assuming that you can only devote limited effort into improving it and must try to maximize the gains from your effort.
    – rwong
    Nov 26, 2017 at 12:35

2 Answers 2

1

Using the backtrace is the best option. There is a proposal to embed the backtrace information into the exception (see it here), but so far nothing is done. To get the backtrace information, you can do like explained in this answer.

If you use boost exceptions, then you can use their diagnostics.

0

First of all, I'm not an experienced C++ developer, my background is mainly Java, so maybe not every idea will work for C++...

I'd mainly go for your third approach, with some adjustment.

Let your exceptions be created without context information, and add the context later. (Or, if some combination of error type and context are found repeatedly, create a convenience method for that combination, if you like.)

I think there can be different types of context: variable IDs, line numbers, ..., and probably a single exception can have multiple contexts, e.g. both a variable ID and a line number (or multiple line numbers if your language has e.g. nested blocks or call stacks).

In your 'variableID' example, it's a variable ID that you want to add as context, and it's available at the place where the exception is created. So, I'd create the exception, then add the variable ID context, and finally throw it.

If, in a method dealing with source line N, you receive an exception from whatever method you called, catch that exception, add the line number N, and rethrow. Or, if it was some foreign exception type, not your Error, first wrap this exception into your Error, then add the line number N and throw.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.