Error handling

Lattenoir has to follow javascript conventions when it comes to error handling. You will face various errors produced by the wrong syntax, range violations, network conditions and other things. These errors rarely provide any useful information - programmer may read the stack, but neither user nor code will know what happened exactly. That's why Lattenoir introduced the concept of the logical error - LNError - with focus on presentability, preservation of details and classification of errors. It is a core lattenoir functionality integrated into both client and server, so the instance of error may be sent from server to client and vice versa.

LNError - Key concepts

LNError - Basic usage

guide
// create
var minimal_err = new LNError();
console.log(minimal_err.code); // null
console.log(minimal_err.payload); // {}
console.log(minimal_err.type_params); // {}

// create 2
var login = 'definitelynotalogin'
var err = new LNError('errors.invalid_login',{login:login, debug_message:'bad login='+login}, {is_invalid_input:1});
// payload.debug_message is handled specially - it is shown on top of stack and message
console.log(err.message);
/*
LNError: bad login=definitelynotalogin

Code: errors.invalid_login

Payload:
    {
        login: "definitelynotalogin"
    }

Type params:
    {
        is_invalid_input: 1
    }
*/

// i18n group is a substring before first dot, no dot means generic group. description key is postfixed by _description
var presentable = err.get_presentable(function(file,code,obj){ return i18n.LR(file,code,obj) }); // pass true as second argument to enable fallback

/*
tried:
1. i18n.LR('errors', 'invalid_login', {login:login, debug_message:'bad login='+login})
2. i18n.LR('generic', 'unknown_error')
3. "Unknown error"
*/
console.log(presentable.title); 

/*
tried:
1. i18n.LR('errors', 'invalid_login_description', {login:login, debug_message:'bad login='+login})
*/
console.log(presentable.description);

err.type_params.is_not_presentable = true; // or code does not exist
presentable = err.get_presentable(function(file,code,obj){ return i18n.LR(file,code,obj) });
/*
tried:
1. i18n.LR('generic', 'unknown_error')
2. "Unknown error"
*/
console.log(presentable.title);

err.type_params=null; // still an object
err.payload=null; // still an object
err.code=null; // null

err.code='unknown_error';
err.payload.prop = ['I am in the array'];

var err_json=JSON.stringify(err);
err = LNError.parse(err_json); // same

err.payload = new Map();
err.payload.set('key','val'); // any object - safe but not serializable
err.payload.get('key');

err = LNError.parse('some string');
console.log(message)
/*
LNError: "some string" (a value of the string primitive)
*/

err = LNError.parse({obj:1});
/*
LNError

Payload:
    {
        obj: 1
    }

Stack:
    ...
*/

// parse & rethrow examples
function do_smth1() {
    // ... here is some code, but we do not know what and if it throws
}
function do_smth2() {
    // ... here is some code, but we do not know what and if it throws
    try {
        do_smth1()
    } catch(err) {
        var lnerr = LNError.parse(err);
        if(!lnerr.code) lnerr.code = 'do_smth2_error';
        lnerr.type_params.do_smth2_prop=1;
        throw lnerr;
    }
}
function do_smth3() {
    // ... here is some code, but we do not know what and if it throws
    try {
        do_smth2()
    } catch(err) {
        var lnerr = LNError.parse(err);
        if(!lnerr.code) lnerr.code = 'do_smth3_error';
        lnerr.type_params.do_smth3_prop=1;
        throw lnerr;
    }
}
try {
    do_smth3()
} catch(err) {
    var lnerr = LNError.parse(err);
    // here full context is available
}

Parsers

  1. exact parse:
    instance of LNError or its child, LNerror in pure object, stringified LNError
  2. any element with _i18n_error_code in some way:
    1. for objects (non-primitives), _i18n_error_code key is passed as code, other props are copied to payload
    2. for strings, _i18n_error_code key is passed as code, other properties are passed to payload
    3. for instanceof Error, if it contains _i18n_error_code, parsed as object, otherwise message is parsed as string.
    Stack is copied, message is copied to debug message if not overwritten
  3. text patterns
    options.text_pattern.dictionary - array of items.
    item.pattern - regex string representation, used for matching
    item.code - saved to code
    item.groups_bind - array of keys, regex-matched groups are bound to these keys in the corresponding order
    for instanceof Error, message is parsed as string, stack is copied, message is copied to debug message if not overwritten
  4. instanceof Error:
    object copied to payload, stack is copied, message is copied to debug message if not overwritten
  5. object, function or primitive

Warning

  • NEVER call delete on members
  • NEVER touch any member that starts with an underscore
  • You may set payload/params to throwable objects. LNError is internally error-resistant, but traversing such objects manually as members may cause exceptions

Errors in lattenoir

Many lattenoir modules, such as SQL, throw such errors. That means you can catch them, parse and extract the details you need. If error cannot be handled, you can add some context, and then rethrow the refined version of error. Sometimes this involves rewriting code and payload based on conditions (such as choosing the deepest logically presentable error). Type params should generally be extended. Done on multiple layers, this catch-extend-rethrow principle is expected to lead to precise and easily classifiable errors. This is illustrated in the basic usage section. Remember - your functions and controllers may be called from other functions and controllers on a future date.

Server

Any object thrown from the top-level controller or prerun causes main sql rollback. Any other response is interpreted as success. throw new LNError from your logic if it encounters an error, or use shortcuts this.throw / this.silent_throw. If you don't care about presentation, throw anything, error will be parsed anyway but some details may be lost.

shortcuts
/*
    variant 1 - up to three arguments: some_string is i18n code
    throw new LNError(some_string, payload, type_params)
*/
this.throw(some_string, payload, type_params);
/*
    variant 2 - always one non-string argument: error is parsed from that
    throw LNError.parse(any_non_string_object)
*/
this.throw(any_non_string_object)

Shortcut usage is illustrated below.

usage example
exports.add=[{
    _type:"controller",
    _config:{name:"UsersList"},
    index: function() {
        // THESE EXAMPLES CONSTRUCT ERROR. FOR SHORTCUTS, typeof a=='string' || b!=undefined || c!=undefined
        // full error, admin notification sent
        var some_login='definitelynotalogin';
        var debug_message = 'message for programmers, part of error.message';
        /*
            generic.invalid_login is key in translator
            generic.invalid_login_description is desctiption
        */
        throw new LNError("invalid_login", {login:some_login, debug_message}, {is_invalid_input:1});
        // equivalent of full error, admin notification sent
        this.throw("invalid_login", {login:some_login, debug_message}, {is_invalid_input:1});
        // same error but without type params, admin notification sent
        this.throw("invalid_login", {login:some_login, debug_message});
        // error without payload, admin notification sent
        this.throw("some_bad_string"); 
        // special case - error without payload but with message from programmers, admin notification sent
        this.throw("some_bad_string", debug_message); 
        // full error, admin notification not sent
        throw new LNError("invalid_login", {login:some_login, debug_message}, {is_invalid_input:1, is_silent:1});
        // equivalent of full error, admin notification not sent
        this.silent_throw("invalid_login", {login:some_login, debug_message}, {is_invalid_input:1}); 
        // ...

        // THESE PARSE ERROR. FOR SHORTCUTS, typeof a!=='string' && b===undefined && c===undefined
        var some_error = new Error("just a js error");
        // admin notification sent
        throw LNError.parse(some_error)
        // same
        this.throw(some_error); 
         // admin notification not sent
        this.silent_throw(some_error);
        // same
        var e = LNError.parse(some_error); e.type_params.is_silent=1; throw e;
         // will be parsed as payload
        this.siltent_throw({prop:1})
        // same - controller wraps errors with LNerror.parse anyway. But maybe you wanted to add some useful information?
        // what if your function or controller is called from some other place?
        throw some_error;
        // Parsed by controller executor, but stack is lost =(
        throw {prop:1};
    },
}];

Usually, request received by lattenoir is directed to the bound controller. That means further logic is wrapped by controller, and controller may throw any kind of error. Controller executor will check authorization, execute controller, catch any controller errors and perform error handling (if you are calling controller from controller or function manually, this logic may be disabled). In case of errors, the following logic is invoked:

  1. Main sql transaction is rolled back
  2. LNError is parsed from the thrown object. Remember - LNError.parse does not lose any data, it transforms everything to LNError. Often thrown object is already LNError, so it is not touched at all.
  3. If error does not have is_silent in its type params (populated by silent_throw or manually) and notifications are enabled, error notification is sent to admins, error type gets flag is_investigated and error_notify_id into its payload. User will be suggested to refer this id to the administrator after render.
  4. Error gets type param show_technical that defines whether user can see technical details of an error. Its absense will also cause sensitive technical details, such as execution stack, to be stripped from error.
  5. For top-level controllers, http code is assigned based on is_silent property and other details.
  6. Some debug info is added
  7. Error is rendered according to the response format (as described below).

For each incoming request server detects an appropriate response format. Due to compatibility reasons, lattenoir will not enforce such format to the controller, but it may tranfsorm the response in some cases. For example, JS object returned from controller will be dumped if client expects HTML, and stringified if client expects JSON. Errors returned from controller are rethrown for top-level controllers. Errors are alaways formatted according to the expected response format.

Errors originated outside controller executor do not have a consistent format. There is a render_http_error JSON interface for internal errors and various HTML or plaintext variations of server errors. These errors are distinguishable by http code and are hardly presentable anyway.

Client

Some of the client-side lattenoir functions also produce and manipulate instances of  LNError.

Functions related to querying data from the server (ajaj*) and handling response (StdResponse) are among them. Their error handling flow is fairly simple but useful:

  1. If client-side or connection error occurs, LNError is created with corresponding error code and debug_message in payload.
  2. If parse error occurs, LNError is created. Original response text is written to payload.response.
  3. If response code is not 2xx or response has top-level property "_instanceof"="LNError", error occured on the server. LNError.parse is called on response. is_serverside=1 type param is set.
  4. All ajaj errors get is_ajaj=1 type_param. ajaj_code (jquery textStatus), http_status type_params are set if possible.
  5. If error handler (errback) was passed, it is called with resulting error as an argument. Otherwise, default handler is invoked. This handler renders error on the client using its translator (with fallback to server localization), preferably in form of notification.

For form requests, if response value has  top-level property "errors", errors are rendered on the form directly and not passed to a callback.

If you define errback, you will get full error details - payload, message and params, just like on the server (except for the sensitive info). Throw on server, catch and handle on the client. Using a callback, you may render error manually via render_lnerror() shortcut.

example
/* server
this.silent_throw('user_does_not_exist', {login:'test', debug_message:'user not found, login=test'}, {can_retry:1});
*/

ajaj_post('/test/', {mode:'do_something_with_user'}, function(ret){
    console.log('success');
}, function(err) {
    if(err.is_serverside) console.log('this is server side error');
    console.log(err.message); // exists
    console.log(err.stack); // exists
    new LNMNotify(render_lnerror(err), {cssclass:'error', timeout:30*1000}); // we can always render it
    if(err.code=='user_does_not_exist') console.log(err.payload.login);
    if(err.code=='user_does_not_exist' && el.type_params.can_retry) { /* some retry logic */ }
})

Compatibility, changelog & migration for your site

Old server + old client: OK
New server + new client: OK
New server + old client: OK
Old server + new client: generally compatible

Old server -> new server: generally compatible
Old client -> new client: migration is required

Server

To enable new capabilities on server, update lattenoir and add use_silent_throw_2 to your site config.

Interface changes:

Client

To enable new capabilities on client, link lnerror.js before js.js script. Set use_legacy_stdresponse flag to force old version of the client logic.

Interface changes:

Migration recommendations:

Go through all your ajaj* and stdresponse* calls and patch them given the new interface. Previously errors were passed to a client as a manually stringified object with error or error_code key. Recommended, but slower option is to change server code. Faster option is usually to do if(ret && (ret.error||ret.error_code)) return StdResponseFuncs.error(ret); in every ajaj call. While stdresponse does not recognize objects with such properties as errors, StdResponse.error can still handle them in a backward-compatible way.