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
- Primary members are code, payload and type_params. code is a string that identifies the logical sense of an error. payload is an object (typeof=='object') that stores properties of the specific error. type_params is an object (constructor==Object) which contains various flags and properties that describe error supertype for easier classification. Use it to describe "is a" relationship, for example "is_sql" to express an SQL error. These are usually additive. Primary members are mutable, you may set or modify them at any time. Exception safety is guaranteed, types are validated under the hood.
- LNError is JSON-serializable / deserializable without losses, it acts like a simple JS object. Also it has technical string representation for advanced users or programmers. Access stack property, and an error will fully dump itself into one readable string.
- LNError provides an ordinary user with a clear presentation of what has happened. It consists of title and description. If error is presentable, tuple (code; payload) is passed to the translator and result is shown to user. Otherwise, unknown error representation is rendered. When LNError travels across the network, the last successful presentation is preserved. It may serve as a fallback if the destination host cannot translate that error but needs to show it.
- LNError inherits from Error. Any instance has properties message, stack, name and has an error prototype, so instanceof Error is always true. Your code that works with Error should also work fine with LNError. Note - that does not apply to the construction of instance, only to its usage. Their constructors have different parameters, so please do not do bulk replacement.
- LNError can be created from anything. Call LNError.parse(any_object), and it will choose the best way to transform object to an error. For LNError or its children, base object is returned without data loss or downcasting to the base LNError class. For Error, stack and message will be copied, and manually assigned properties are preserved. Given the object or function, it will save that to the internal storage. It will attempt to deserialize strings in multiple ways, looking for its own JSON representation and templates. If nothing succeeds, primitive is wrapped inside message. Stack is created on the fly unless available.
LNError - Basic usage
// 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
- exact parse:
instance of LNError or its child, LNerror in pure object, stringified LNError -
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 payload2. for strings, _i18n_error_code key is passed as code, other properties are passed to payload3. 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
- text patterns:
options.text_pattern.dictionary - array of items.item.pattern - regex string representation, used for matchingitem.code - saved to codeitem.groups_bind - array of keys, regex-matched groups are bound to these keys in the corresponding orderfor instanceof Error, message is parsed as string, stack is copied, message is copied to debug message if not overwritten
-
instanceof Error:
-
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.
/*
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.
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:
- Main sql transaction is rolled back
- 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.
- 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.
- 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.
- For top-level controllers, http code is assigned based on is_silent property and other details.
- Some debug info is added
- 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.
- html_slice - some text that will be rendered as part of html or as entire html document. Errors are rendered in that HTML like other elements.
-
json_polymorphic - client expects JSON string and will receive JSON representation of error if it occurs. Client is responsible for handling and displaying that error. For old clients, HTML representation is always avaliable under the error key. To know exactly why this type is called polymorphic, please see the section about compatibility.
-
ajajform_iframe - return value is an HTML document with embedded script, in which arguments are defined by the response of controller. In this case, object representation of LNError will be passed.
-
plain - returns controller output as-is. This is for manually called controllers, and LNError instance will be returned if error occurs.
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:
- If client-side or connection error occurs, LNError is created with corresponding error code and debug_message in payload.
- If parse error occurs, LNError is created. Original response text is written to payload.response.
- 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.
- All ajaj errors get is_ajaj=1 type_param. ajaj_code (jquery textStatus), http_status type_params are set if possible.
- 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.
/* 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:
- Added LNError base class, core modules now throw instances of LNError
- Top-level controller executor is always launched in nothrow mode (previously that was not the case with ajaj and
LNRewriter)
- Introduced response formats to RR
- Soft formatting is performed on controller return value
- Formatting is performed on errors. Previously, lattenoir was able to send JSON error in the following conditions: ajaj field is present, error is a JSON string, request did not go through rewriter. Such error has type interface {error:{ error_title:String, error:String|Object }, error_code:String } which was used quite often. Format json_polymorphic (which is now default for ajaj) for errors is JSON representation of LNError merged with JSON representation of the legacy error. New clients will interpret it as LNError because there is a key "_instanceof", while old clients will also interpret it as an error because it follows legacy interface.
- Http codes in edge cases were revamped
- New Dumper and admin-notifier implemented (some arguments are now ignored)
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:
- ajaj*: errorback always gets one parameter - LNError instance. Previously it could be a string, an object parsed from response, or jquery XHR object
- ajaj_form_error - logically replaced with lnerror_parse_ajaj_error+StdResponse.error
- ajaj*: errback is not called if non-standard is provided, use opts.call_std_error to force std errback
-
ajaj*: return type json is enforced
- StdResponse: for new errors, LNMNotify is the default notification method, and dialog window is used as a fallback
- StdResponse: JSON with keys error or error_code is interpreted as success, not an error
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.