You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

358 lines
15 KiB

=============
Error objects
=============
Ecmascript allows throwing of arbitrary values, although most user code
throws objects inheriting from the ``Error`` constructor. Ecmascript
``Error`` instances are quite barebones: they only contain a ``name``
and a ``message``. Most Ecmascript implementations provide additional
error properties like file name, line number, and traceback.
This document describes how Duktape creates ``Error`` objects and what
properties such objects have. The internal traceback data format and
the mechanism for providing human readable tracebacks is also covered.
Also see the user documentation which covers the exposed features in a
more approachable way.
Error object creation
=====================
Duktape "augments" any errors at their creation. Throwing, catching,
and re-throwing have no impact on the error object. Only error values
which are instances of ``Error`` are augmented, other kinds of values
(even objects) are left alone.
Errors can be created in multiple ways:
* From Ecmascript code by creating an error, usually (but not always) tied
to a ``throw`` statement, e.g.::
throw new Error('my error');
In this case the Error object should capture the file and line of the
file creating the Error object (with ``new Error(...)``).
* From C code using the Duktape API, e.g.::
duk_error(ctx, DUK_ERR_RANGE_ERROR, "invalid argument: %d", argvalue);
In these cases the ``__FILE__`` and ``__LINE__`` of the throw site are
very useful. API calls which create an error object are implemented as
macros to capture ``__FILE__`` and ``__LINE__`` conveniently. This is
very important to create useful tracebacks.
* From inside the Duktape implementation, usually with the ``DUK_ERROR()``
macro, e.g.::
DUK_ERROR(thr, DUK_ERR_TYPE_ERROR, "invalid argument");
In this case the Duktape internal file and line is useful and must be
captured. However, it is not "blamed" as the source of the error as far
as filename and line number of the error are concerned (after all, the
user doesn't usually care about the internal line numbers).
When errors are thrown using the Duktape API or from inside the Duktape
implementation, the value thrown is always an instance of ``Error`` and
is therefore augmented. Error creation and throwing happens at the same
time.
When errors are thrown from Ecmascript code the situation is different.
There is nothing preventing user code from separating the error creation
and error throwing from each other::
var err = new Error('value too large');
if (arg >= 100) {
throw err;
}
In fact, the user may never intend to throw the error but may still want
to access the traceback::
var err = new Error('currently here');
print('debug: reached this point\n' + err.stack);
Augmenting an error when it is thrown has several complications; for instance,
the error might accidentally be augmented again (overwriting previous values)
when it is rethrown. Some error values may never even be thrown, but would
still benefit from having traceback information.
Duktape augments an error object *when it is created*. In more concrete terms,
when a constructor call is made (i.e. ``new Foo()``) the final result which is
about to be returned to calling code is inspected. This is a change to the
standard handling of constructor calls and applies uniformly whenever any
object is created (and unfortunately carries some overhead). The value gets
augmented with error information (e.g. tracedata) if it fulfills the following
criteria:
1. The value is an extensible object.
2. The value is an instance of ``Error``, i.e. ``Error.prototype`` is in its
internal prototype chain.
Duktape refuses to add additional fields to the object if it already contains
fields of the same name. For instance, if the created object has a ``tracedata``
field, it won't get overwritten by the augmentation process.
Since a particular object is never constructed twice, the error object is not
changed by catching, re-throwing, etc. The downside of augmenting during
creation is that the error information may not accurately reflect the actual
``throw`` statement which throws the error. In particular, user code may
create an error value in a completely different place at a completely different
time than where and when the error is actually thrown. User code may even throw
the same error value multiple times.
Error objects can also be created by calling the ``Error`` constructor (or a
constructor of a subclass) as a normal function. In the standard this is
semantically equivalent to a constructor call. Duktape will also augment an
error created by calling a built-in error constructor with a normal function
call. However, any Error sub-classes created by the user don't exhibit this
behavior. For instance::
MyError = function(msg) { this.message = msg; this.name = 'MyError'; return this; }
MyError.prototype = Error.prototype;
var e1 = new Error('test 1'); // augmented, constructor call
var e2 = Error('test 2'); // augmented, special handling
var e3 = new MyError('test 3'); // augmented, constructor call
var e4 = MyError('test 4'); // not augmented
print(e1.stack);
print(e2.stack);
print(e3.stack);
print(e4.stack);
Prints out::
Error: test 1
global test.js:4 preventsyield
Error: test 2
Error (null) native strict preventsyield
global test.js:5 preventsyield
MyError: test 3
global test.js:6 preventsyield
undefined
Note that because of internal details, the traceback is different for the
``Error`` constructor when it is called as a normal function.
Fixing this behavior so that even user errors get augmented when called with
a non-constructor call seems difficult. It would be difficult to detect
when augmentation is appropriate and it would also add overhead to every
normal function call.
Error object properties
=======================
The following table summarizes properties of ``Error`` objects constructed
within the control of the implementation:
+-----------------+----------+--------------------------------------------+
| Property | Standard | Description |
+=================+==========+============================================+
| name | yes | e.g. ``TypeError`` for a TypeError |
| | | (usually inherited) |
+-----------------+----------+--------------------------------------------+
| message | yes | message given when constructing (or empty) |
| | | (own property) |
+-----------------+----------+--------------------------------------------+
| fileName | no | name of the file where constructed |
| | | (inherited accessor) |
+-----------------+----------+--------------------------------------------+
| lineNumber | no | line of the file where constructed |
| | | (inherited accessor) |
+-----------------+----------+--------------------------------------------+
| stack | no | printable stack traceback string |
| | | (inherited accessor) |
+-----------------+----------+--------------------------------------------+
| tracedata | no | stack traceback data, internal raw format |
| | | (own property) |
+-----------------+----------+--------------------------------------------+
The ``Error.prototype`` contains the following non-standard properties:
+-----------------+----------+--------------------------------------------+
| Property | Standard | Description |
+=================+==========+============================================+
| stack | no | Accessor property for getting a printable |
| | | traceback based on tracedata. |
+-----------------+----------+--------------------------------------------+
| fileName | no | Accessor property for getting a filename |
| | | based on tracedata. |
+-----------------+----------+--------------------------------------------+
| lineNumber | no | Accessor property for getting a linenumber |
| | | based on tracedata. |
+-----------------+----------+--------------------------------------------+
All of the accessors are in the prototype in case the object instance does
not have an "own" property of the same name. This allows for flexibility
in minimizing the property count of error instances while still making it
possible to provide instance-specific values when appropriate. Note that
the setters allow user code to write an instance-specific value as an "own
property" of the error object, thus shadowing the accessors in later reads.
Notes:
* The ``stack`` property name is from V8 and behavior is close to V8.
V8 allows user code to write to the ``stack`` property but does not
create an own property of the same name. The written value is still
visible when ``stack`` is read back later.
* The ``fileName`` and ``lineNumber`` property names are from Rhino.
* The ``tracedata`` has an internal format which may change from version
to version (even build to build). It should never be serialized or
used outside the life cycle of a Duktape heap.
* In size-optimized builds traceback information may be omitted. In such
cases ``fileName`` and ``lineNumber`` are concrete own properties.
* In size-optimized builds errors created by the Duktape implementation
will not have a useful ``message`` field. Instead, ``message`` is set
to a string representation of the error ``code``. Exceptions thrown
from user code will carry ``message`` normally.
Cause chains
============
There is currently no support for cause chains: Ecmascript doesn't have a
cause chain concept nor does there seem to be an unofficial standard for
them either.
A custom cause chain could be easily supported by allowing a ``cause``
property to be set on an error, and making the traceback formatter obey it.
A custom mechanism for setting an error cause would need to be used.
A very non-invasive approach would be something like::
try {
f();
} catch (e) {
var e2 = new Error("something went wrong"); // line N
e2.cause = e; // line N+1
throw e2; // line N+2
}
This is quite awkward and error line information is easily distorted.
The line number issue can be mitigated by putting the error creation
on a single line, at the cost of readability::
try {
f();
} catch (e) {
var e2 = new Error("something went wrong"); e2.cause = e; throw e2;
}
One could also extend the error constructor to allow a cause to be specified
in a constructor call. This would mimic how Java works and would be nice to
use, but would have more potential to interfere with standard semantics::
try {
f();
} catch (e) {
throw new Error("something went wrong", e);
}
Using a setter method inherited from ``Error.prototype`` would be a very bad
idea as any such calls would be non-portable and cause errors to be thrown
when used in other Ecmascript engines::
try {
f();
} catch (e) {
var e2 = new Error("something went wrong", e);
e2.setCause(e); // throws error if setCause is undefined!
throw e2;
}
Since errors are also created (and thrown) from C code using the Duktape
API and from inside the Duktape implementation, cause handling would need
to be considered for these too.
Because the ``cause`` property can be set to anything, the implementation
would need to tolerate e.g.::
// non-Error causes (print reasonably in a traceback)
e.cause = 1;
// cause loops (detect or sanity depth limit traceback)
e1.cause = e2;
e2.cause = e1;
Traceback format (tracedata)
============================
The purpose of the ``tracedata`` value is to capture the relevant call stack
information very quickly before the call stack is unwound by error handling.
In many cases the traceback information is not used at all, so it should be
recorded in a compact and cheap manner.
To fulfill these requirements, the current format, described below, is a bit
arcane. The format is version dependent, and is not intended to be accessed
directly by user code. The implementation should provide stable helpers for
getting e.g. readable tracebacks or inspecting the traceback entries.
The ``tracedata`` value is a flat array, populated with values describing
the contents of the call stack, starting from the call stack top and working
downwards until either the call stack bottom or the maximum traceback depth
is reached.
If a call has a related C ``__FILE__`` and ``__LINE__`` those are first
pushed to ``tracedata``:
* The ``__FILE__`` value as a string.
* A number (double) containing the expression::
(flags << 32) + (__LINE__)
The only current flag indicates whether or not the ``__FILE__`` /
``__LINE__`` pair should be "blamed" as the error location when the user
requests for a ``fileName`` or ``lineNumber`` related to the error.
After that, for each call stack element, the array entries appended to
``tracedata`` are pairs consisting of:
* The function object of the activation. The function object contains the
function type and name. It also contains the filename (or equivalent, like
"global" or "eval") and possibly PC-to-line debug information. These are
needed to create a printable traceback.
* A number (double) containing the expression::
(activation_flags << 32) + (activation_pc)
For C functions, the program counter value is zero. Activation flag
values are defined in ``duk_hthread.h``. The PC value can be converted
to a line number with debug information in the function object. The
flags allow e.g. tailcalls to be noted in the traceback.
The default ``Error.prototype.stack`` accessor knows how to convert this
internal format into a human readable, printable traceback string. It is
currently the only function processing the tracedata, although it would be
useful to provide user functions to access or decode elements of the
traceback individually.
Notes:
* An IEEE double can hold a 53-bit integer accurately so there is space
for plenty of flags in the current representation. Flags must be in
the low end of the flags field though (bit 20 or lower)
* The number of elements appended to the ``tracedata`` array for each
activation does not need to constant, as long as the value can be decoded
starting from the beginning of the array (in other words, random access is
not important at the moment).
* The ``this`` binding, if any, is not currently recorded.
* The variable values of activation records are not recorded. They would
actually be available because the call stack can be inspected and register
maps (if defined) would provide a way to map identifier names to registers.
This is definitely future work and may be needed for better debugging
support.
* The ``tracedata`` value is currently an array, but it may later be changed
into an internal type of its own right to optimize memory usage and
performance. The internal type would then basically be a typed buffer
which garbage collection would know how to visit.