After an evaluation, GNOME has moved from Bugzilla to GitLab. Learn more about GitLab.
No new issues can be reported in GNOME Bugzilla anymore.
To report an issue in a GNOME project, go to GNOME GitLab.
Do not go to GNOME Gitlab for: Bluefish, Doxygen, GnuCash, GStreamer, java-gnome, LDTP, NetworkManager, Tomboy.
Bug 591480 - Allow accessing code/domain from thrown GErrors
Allow accessing code/domain from thrown GErrors
Status: RESOLVED FIXED
Product: gjs
Classification: Bindings
Component: general
unspecified
Other Linux
: Normal normal
: ---
Assigned To: gjs-maint
gjs-maint
: 602449 (view as bug list)
Depends on: 602516
Blocks: 641174 671169
 
 
Reported: 2009-08-11 19:02 UTC by Owen Taylor
Modified: 2012-06-07 18:42 UTC
See Also:
GNOME target: ---
GNOME version: ---


Attachments
add code/domain properties to Error for thrown GError (5.87 KB, patch)
2011-02-14 00:12 UTC, Maxim Ermilov
none Details | Review
add code/domain properties to Error for thrown GError (7.12 KB, patch)
2011-03-03 22:56 UTC, Maxim Ermilov
needs-work Details | Review
Introduce special marshalling for GErrors (37.57 KB, patch)
2012-03-02 21:09 UTC, Giovanni Campagna
reviewed Details | Review
GError: add stack, fileName and lineNumber (13.01 KB, patch)
2012-03-03 17:18 UTC, Giovanni Campagna
committed Details | Review
Introduce special marshalling for GErrors (38.13 KB, patch)
2012-06-06 16:44 UTC, Giovanni Campagna
reviewed Details | Review
Introduce special marshalling for GErrors (40.13 KB, patch)
2012-06-07 17:21 UTC, Giovanni Campagna
committed Details | Review

Description Owen Taylor 2009-08-11 19:02:20 UTC
When we throw a GError we should allow access to the code/domain so that specific errors can be identified.

(Colin's patch in 589651 adds gjs_throw_gerror, but just throws the message using gjs_throw())

This probably means throwing a different class from Error() (a subclass?), though it might also be possible to just stick extra properties onto the created Error object.
Comment 1 Owen Taylor 2009-11-19 21:05:27 UTC
*** Bug 602449 has been marked as a duplicate of this bug. ***
Comment 2 Owen Taylor 2009-11-19 21:06:24 UTC
Dan points out in bug 602449 that the original error message is also interesting, since the stringified version of the exception contains excess noise.
Comment 3 Owen Taylor 2009-12-21 22:06:44 UTC
Working through this in some more detail, commenting in chunks because there's no draft saving in Bugzilla.

Preferrably the stringification of the exception would give the domain and code. This is easy enough to do if we want:

 g-shell-error-quark: 0: Quoted text doesn't begin with a quotation mark

But getting to:

 GLib.ShellError.BAD_QUOTING: Quoted text doesn't begin with a quotation mark

is somewhat harder. There's some rudimentary support for enum to error quark mappings in gobject-introspection, but it doesn't seem finished:

 - The glib:error-quark enumeration is only emitted into the output for introspected enumerations; there's no good reason for this, except that glibtransformer.py:_resolve_quarks() works off of the list of GObject registered type names instead of going off all known type names.

 - The glib:error-quark value gives a function name; maybe this is OK, but it requires calling all the error quark functions we know about to try and find a match. (That would call g_quark_from_static_string(), page in program text, allocate hash nodes, etc.) It might be handy to have the atom value in addition. *Or* we could try to derive function name (or type name) from the quark value at run time. But:

There are some consistency problems in the quark names. Looking at GLib and Gio:

 Quark value                 Enum name          Function returning quark

 g-key-file-error-quark      GKeyFileError      g_key_file_error_quark
 g-markup-error-quark        GMarkupError       g_markup_error_quark
 g-bookmark-file-error-quark GBookmarkFileError g_bookmark_file_error_quark
 g-file-error-quark          GFileError         g_file_error_quark
 g-shell-error-quark         GShellError        g_shell_error_quark
 g-regex-error-quark         GRegexError        g_regex_error_quark
 g-io-channel-error-quark    GIOChannelError    g_io_channel_error_quark
 g-resolver-error-quark      GResolverError     g_resolver_error_quark

But:

 g-option-context-error-quark GOptionError   g_option_error_quark
 g_thread_error               GThreadError   g_thread_error_quark
 g_convert_error              GConvertError  g_convert_error_quark
 g-exec-error-quark           GSpawnError    g_spawn_error_quark
 g-io-error-quark             GIOErrorEnum   g_io_error_quark

Wouldn't be that hard to handle that range of enums, and most of them could be fixed over the long run, if nobody is already depending on the quark values (I guess a language binding might be?)
Comment 4 Owen Taylor 2009-12-21 22:31:35 UTC
One of the fundamental questions here is what is the style we expect.

 try {
 } catch (e) {
   if (e instanceof GjsError && 
       e.domain = GLib.SHELL_ERROR && e.code == GLib.ShellError.BAD_QUOTING) {
       /* do something */       
   } else {
       throw e;
   }
 }

[ GLib.SHELL_ERROR or GLib.shell_error_quark() aren't defined at the moment in the .gir, not sure why the second isn't there ]

[...]
   if (e.domain == GLib.SHELL_ERROR && e.code == GLib.ShellError.BAD_QUOTING) {
[...]

[...]
   if (e.name == "GLib.ShellError.BAD_QUOTING")
[...]

Or using a JS 1.5 addition:

 try {
 } catch (e if e.name == "GLib.ShellError.BAD_QUOTING")
     /* do something */
 }

The first with the instanceof *and* the checks on domain/code is pretty horrifying, so that makes me feel that we could get away without a subclass. The advantage of not using a subclass is that we easily get filename/linenumber information if we throw an Error, while it's quote hard to subclass and still have that ability.
Comment 5 Dan Winship 2009-12-22 15:42:56 UTC
see bug 602512 (non-gtype-registered error domains don't get marked) and bug 602516 (error domain info doesn't end up in typelib).

another possibility btw is:

    if (e.domain == GLib.ShellError && e.code == GLib.ShellError.BAD_QUOTING)

(ie, error->domain gets translated into the js object that the enum values are stored on). This preserves the "domain is a prefix of code" property that it has in C.
Comment 6 Maxim Ermilov 2011-02-14 00:12:08 UTC
Created attachment 180794 [details] [review]
add code/domain properties to Error for thrown GError

e.domain - error domain string
e.code - error code
Comment 7 Owen Taylor 2011-03-01 17:32:57 UTC
(In reply to comment #6)
> Created an attachment (id=180794) [details] [review]
> add code/domain properties to Error for thrown GError
> 
> e.domain - error domain string
> e.code - error code

Hmm, I guess that sort of works, but I don't think it's good enough.

 454	assertEquals(x.domain, "g-io-error-quark");

is clarly an internal detail leaking out not an API. Someone is going to have to sort through the mess in 602516 from last year and figure out what to do so we actually have a back-mapping from the string to the public API.

(In reply to comment #5)
> see bug 602512 (non-gtype-registered error domains don't get marked) and bug
> 602516 (error domain info doesn't end up in typelib).
> 
> another possibility btw is:
> 
>     if (e.domain == GLib.ShellError && e.code == GLib.ShellError.BAD_QUOTING)
> 
> (ie, error->domain gets translated into the js object that the enum values are
> stored on). This preserves the "domain is a prefix of code" property that it
> has in C.We really  

Hmm, I sort of like this, though, Gio.IOErrorEnum is an ugly case. (We really should have de-worked around the clash with G.IOError at GIR generation time rather than leaking that workaround to the GIR API, but I don't know if we have the facility for doing this and it's possibly too late now.)

Note that it doesn't solve the subclassing problem - you can't write 

 except (e if e.domain == GLib.ShellError && e.code == GLib.ShellError.BAD_QUOTING)

Because that will produce warnings in SpiderMonkey strict mode about e.domain being undefined. The e.name thing I suggested is using the fact that name is a standard Javascript property, but is maybe a bit hacky/ugly. 

 except (e if e instanceof GjsError && 
              e.domain == GLib.ShellError && 
              e.code == GLib.ShellError.BAD_QUOTING)

isn't _that_ bad. Dunno.
Comment 8 Tommi Komulainen 2011-03-01 17:47:54 UTC
one more alternative might be to export g_error_matches() in a way, so you could do something like

if (GLib.Error.matches(e, GLib.ShellError, GLib.ShellError.BAD_QUOTING))

or maybe even drop the somewhat redundant prefix:

if (GLib.Error.matches(e, GLib.ShellError.BAD_QUOTING))
Comment 9 Owen Taylor 2011-03-01 17:54:36 UTC
(In reply to comment #8)
> one more alternative might be to export g_error_matches() in a way, so you
> could do something like
> 
> if (GLib.Error.matches(e, GLib.ShellError, GLib.ShellError.BAD_QUOTING))
> 
> or maybe even drop the somewhat redundant prefix:
> 
> if (GLib.Error.matches(e, GLib.ShellError.BAD_QUOTING))

GLib.ShellError.BAD_QUOTING is just a number
Comment 10 Tommi Komulainen 2011-03-01 18:02:15 UTC
(In reply to comment #9)
> 
> GLib.ShellError.BAD_QUOTING is just a number

Sure, but I'm not sure it's an absolute requirement. If it makes sense to handle domain and code separately, then sure, but otherwise it could be an object encoding both domain and code into one. But maybe not worth the trouble.
Comment 11 Tommi Komulainen 2011-03-01 18:04:05 UTC
Actually another option I didn't see would be to have each domain mapped to a class, then I think the use could be maybe bit more javascript-like, e.g.

except (e if e instanceof GLib.ShellError)
Comment 12 Maxim Ermilov 2011-03-03 22:56:56 UTC
Created attachment 182415 [details] [review]
add code/domain properties to Error for thrown GError

Allow to do:
    catch (x if x.domain && x.domain == Gio.IOErrorEnum)

(depends on 2 patches from 602516)
Comment 13 Dan Winship 2011-08-12 15:42:03 UTC
Comment on attachment 182415 [details] [review]
add code/domain properties to Error for thrown GError

so in this patch, "e.domain == GLib.IOErrorEnum" only works because you changed toString() on GLib.IOErrorEnum, which seems like a kludge.

also, at some point we may want to be able to throw GErrors as well (not sure if throwing errors from callbacks is supported, but if we ever implement subclassing we'd need to be able to do it).

So I think we should do it like Tommi suggested; when creating an enum object, if it has an error-domain, make the enum object be a subclass of Error, and when converting a GError to an exception, create an error of the appropriate class. And then we can just do

    except (e if (e instanceof GLib.ShellError && 
                  e.code == GLib.ShellError.BAD_QUOTING))

and in the future,

    throw new GLib.ShellError(GLib.ShellError.BAD_QUOTING, _("Blah blah blah"));

Actually, we probably want GLib.Error, which is a subclass of Error, and each error domain is a subclass of GLib.Error, so we can distinguish errors-translated-from-glib from errors-thrown-by-javascript, if need be.
Comment 14 Giovanni Campagna 2012-03-01 21:32:20 UTC
All of this was nice in theory, but never implemented, as it is pretty complex to do (you need the same dynamic subclassing as boxed and object)
As a stop gap for 3.4, for apps that want to handle GErrors right now, can we get this patch merged?

(If overriding toString() is perceived bad, and it probably is, JSExtendedClass has a JSEqualityOp to override the behavior of ==)
Comment 15 Giovanni Campagna 2012-03-02 21:08:26 UTC
Ok, it turned out to be easier than I thought.
The next patch implements the proposed behaviour, with error enums that become classes of which the exception are instances.

I though of supporting vala style "throw new Gio.IOErrorEnum.CANCELLED(message)", but then implemented the simpler "throw new Gio.IOErrorEnum({ code: Gio.IOErrorEnum.CANCELLED, message: message })". Actually, there is little use for throwing GErrors, since they're not yet propagated to C callers.

On the catching side, the usage pattern is
try {
   fail();
} catch(e if e instanceof Gio.IOErrorEnum &&
             e.code == Gio.IOErrorEnum.CANCELLED) {
   print("Operation cancelled");
} catch(e) {
   print("Something else happened: " + e);
}

Finally, such classes don't inherit from Error. Instead, they inherit from GLib.Error, the standard GI boxed class. This means for example that the above can be written as
catch(e if e.matches(Gio.io_error_quark(), Gio.IOErrorEnum.CANCELLED))
Comment 16 Giovanni Campagna 2012-03-02 21:09:11 UTC
Created attachment 208882 [details] [review]
Introduce special marshalling for GErrors

Previously GErrors were transformed into plain Error with a custom
message, which removed the code and domain metadata. This commit
introduces a new class hierarchy (derived from GLib.Error) for each
enumeration representing an error domain, and modifies existing
code to throw instances of that when a function fails.
Comment 17 Dan Winship 2012-03-03 13:43:54 UTC
(In reply to comment #15)
> Finally, such classes don't inherit from Error. Instead, they inherit from
> GLib.Error, the standard GI boxed class.

This means they don't get the "stack" property with a stack trace like "real" errors do... but I think it's pretty easy to generate that
Comment 18 Giovanni Campagna 2012-03-03 17:18:08 UTC
Created attachment 208909 [details] [review]
GError: add stack, fileName and lineNumber

Similar to native Errors(), GLib.Error is extended to provide debug
information in the form of fileName, lineNumber and stack (obtained
using the JS debug API). At the same time, the existing stack logging
facility is modified to be similar in format to the native one, and
logError is modified to avoid iterating object properties (which
gives an undefined order, and does not include prototype properties)
Comment 19 Jasper St. Pierre (not reading bugmail) 2012-03-27 16:03:22 UTC
I don't see why we're inheriting from GLib.Error and emulating the JS Error class, rather than just inheriting from the JS Error class.
Comment 20 Giovanni Campagna 2012-03-27 16:35:42 UTC
Because you cannot "inherit" from a JS Error class. What you can do is create a custom class that happens to have .__proto__ = Error.prototype, but that won't give you special properties (they are set in the constructor). BTW, none of the other standard error classes (TypeError, RangeError, SyntaxError, EvalError, ReferenceError, URIError) inherit from Error.prototype, or even share it.
On the other hand, inheriting from GLib.Error (as much as allowed by JS) means that "e instanceof GLib.Error" works, and "e.matches(domain, code)" works.
Comment 21 Jasper St. Pierre (not reading bugmail) 2012-03-27 16:42:58 UTC
(In reply to comment #20)
> Because you cannot "inherit" from a JS Error class. What you can do is create a
> custom class that happens to have .__proto__ = Error.prototype

That's inheritance in JS, isn't it?

> but that won't
> give you special properties (they are set in the constructor).

So call the parent constructor in our constructor? (Well, it may be not worth it from C.)

> BTW, none of the
> other standard error classes (TypeError, RangeError, SyntaxError, EvalError,
> ReferenceError, URIError) inherit from Error.prototype, or even share it.

    > TypeError.prototype.__proto__ == Error.prototype
    true
Comment 22 Giovanni Campagna 2012-03-27 16:48:10 UTC
(In reply to comment #21)
> (In reply to comment #20)
> > Because you cannot "inherit" from a JS Error class. What you can do is create a
> > custom class that happens to have .__proto__ = Error.prototype
> 
> That's inheritance in JS, isn't it?

Yeah, but doing it with objects having different JSClass is a call for trouble (just look at GIRepositoryFunction.toString()).

> > but that won't
> > give you special properties (they are set in the constructor).
> 
> So call the parent constructor in our constructor? (Well, it may be not worth
> it from C.)

Uhm... How? Error() returns a new instance, with a different prototype and JSClass.

> > BTW, none of the
> > other standard error classes (TypeError, RangeError, SyntaxError, EvalError,
> > ReferenceError, URIError) inherit from Error.prototype, or even share it.
> 
>     > TypeError.prototype.__proto__ == Error.prototype
>     true

Ugh, you're right... Probably I made a mistake last time I checked.
Comment 23 Jasper St. Pierre (not reading bugmail) 2012-05-25 19:47:31 UTC
It's probably worth looking at how Gecko creates custom error subclasses internally.
Comment 24 Giovanni Campagna 2012-05-25 21:29:25 UTC
For JS Errors:

http://mxr.mozilla.org/mozilla-central/source/js/src/jsexn.cpp#812
They call InitErrorClass for each of the derived class, which essentially does a JS_InitClass using the internal API, passing the same constructor as Error(), and redefining special properties.

For XP Connect:

http://mxr.mozilla.org/mozilla-central/source/js/xpconnect/src/XPCThrower.cpp#246
As I read it, they just wrap the native object without special code.
Comment 25 Jasper St. Pierre (not reading bugmail) 2012-05-25 21:33:40 UTC
Hm, I thought the HTML5 specification (or something else like Web Workers) defined some error subclasses that aren't part of ECMA-262.
Comment 26 Giovanni Campagna 2012-05-25 22:13:56 UTC
If you're referring to DOMException, all I could find is
http://mxr.mozilla.org/mozilla-central/source/dom/base/nsDOMException.cpp
http://mxr.mozilla.org/mozilla-central/source/js/xpconnect/src/XPCException.cpp#269
http://mxr.mozilla.org/mozilla-central/source/js/xpconnect/src/XPCStack.cpp#97

which seems to show they reimplement it at the C++ level and then use XPConnect to bring it back to JS.
Comment 27 Jasper St. Pierre (not reading bugmail) 2012-05-25 23:51:17 UTC
Review of attachment 208909 [details] [review]:

I'm not a fan of this, but I guess we have no other real option until the SpiderMonkey developers give us a new API. They said they were going to because they had a need internally in Gecko to create actual Error subclasses.
Comment 28 Jasper St. Pierre (not reading bugmail) 2012-05-25 23:53:37 UTC
Review of attachment 208882 [details] [review]:

Mostly good.

::: gi/arg.c
@@ +1343,3 @@
                     !g_type_is_a(gtype, G_TYPE_CLOSURE)) {
+
+                    if (gtype == G_TYPE_ERROR) {

g_type_is_a

::: gi/gerror.c
@@ +67,3 @@
+    GJS_NATIVE_CONSTRUCTOR_PRELUDE(error);
+
+    /* Check early to avoid allocating memory for nothing */

You've already constructed a new object in the prelude, so you're still allocating memory. Do this before the prelude.

@@ +75,3 @@
+    priv = g_slice_new0(Error);
+
+    GJS_INC_COUNTER(boxed);

Define a new counter.

@@ +145,3 @@
+
+static JSBool
+error_getter(JSContext *context, JSObject *obj, jsid id, jsval *vp)

Would make more sense to me to have three difference getters instead of one.

@@ +151,3 @@
+    int tinyid;
+
+    /* Optimization: let JS cache the value */

Not sure I understand this.

@@ +220,3 @@
+
+        if (!gjs_string_from_utf8(context, descr, -1, &v_out)) {
+            g_free(descr);

I'd prefer a standard "goto out;" strategy.
Comment 29 Jasper St. Pierre (not reading bugmail) 2012-06-06 04:29:55 UTC
Giovanni: ping?
Comment 30 Giovanni Campagna 2012-06-06 16:04:41 UTC
Sorry, it slipped out of my sight.

(In reply to comment #28)
> @@ +151,3 @@
> +    int tinyid;
> +
> +    /* Optimization: let JS cache the value */
> 
> Not sure I understand this.

After the getter runs, the engine takes the returned jsval and stores it. Since properties don't change dynamically, the stored value is always good, so we can avoid marshalling the same value over it.

Revised patch coming...
Comment 31 Giovanni Campagna 2012-06-06 16:44:40 UTC
Created attachment 215760 [details] [review]
Introduce special marshalling for GErrors

Previously GErrors were transformed into plain Error with a custom
message, which removed the code and domain metadata. This commit
introduces a new class hierarchy (derived from GLib.Error) for each
enumeration representing an error domain, and modifies existing
code to throw instances of that when a function fails.
Comment 32 Jasper St. Pierre (not reading bugmail) 2012-06-06 19:21:52 UTC
Review of attachment 215760 [details] [review]:

Looks mostly fine.

::: gi/function.c
@@ +675,3 @@
         g_assert_cmpuint(0, <, c_argc);
 
         if (type == GI_INFO_TYPE_STRUCT || type == GI_INFO_TYPE_BOXED) {

I wonder if we could reuse some of the same code from arg.c here.

@@ +679,3 @@
+
+            /* GError must be special cased */
+            if (gtype == G_TYPE_ERROR)

g_type_is_a

::: gi/value.c
@@ +391,3 @@
             obj = JSVAL_TO_OBJECT(value);
+
+            if (gtype == G_TYPE_ERROR) {

g_type_is_a

::: test/js/testEverythingBasic.js
@@ +528,3 @@
+    } catch (x) {
+	assertTrue(x instanceof Gio.IOErrorEnum);
+	assertTrue(x.matches(Gio.io_error_quark(), Gio.IOErrorEnum.NOT_FOUND));

I don't really like the Gio.io_error_quark() syntax, but sure.

Maybe you could somehow add a Gio.IOErrorEnum.$error_quark so that you could do:

    x.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND);

still suboptimal, but until we get an actual class for enumerations, it's the best we can do.
Comment 33 Giovanni Campagna 2012-06-07 16:55:03 UTC
(In reply to comment #32)
> Review of attachment 215760 [details] [review]:
> 
> Looks mostly fine.
> 
> ::: gi/function.c
> @@ +675,3 @@
>          g_assert_cmpuint(0, <, c_argc);
> 
>          if (type == GI_INFO_TYPE_STRUCT || type == GI_INFO_TYPE_BOXED) {
> 
> I wonder if we could reuse some of the same code from arg.c here.

arg.c "public" API is in terms of GIArgInfo, not GITypeInfo, so at least not now. Next patch, maybe.
Comment 34 Giovanni Campagna 2012-06-07 17:21:59 UTC
Created attachment 215858 [details] [review]
Introduce special marshalling for GErrors

Previously GErrors were transformed into plain Error with a custom
message, which removed the code and domain metadata. This commit
introduces a new class hierarchy (derived from GLib.Error) for each
enumeration representing an error domain, and modifies existing
code to throw instances of that when a function fails.

About the GQuark stuff, I didn't want to add more special cases in
arg.c, so I went with Gio.IOErrorEnum.valueOf() returning the GQuark.
Also, I added Error.prototype.matches(), allowing for a final API like:
catch(e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)), which is
neat, IMHO
Comment 35 Jasper St. Pierre (not reading bugmail) 2012-06-07 17:39:28 UTC
(In reply to comment #34)
> Created an attachment (id=215858) [details] [review]
> Introduce special marshalling for GErrors

> About the GQuark stuff, I didn't want to add more special cases in
> arg.c, so I went with Gio.IOErrorEnum.valueOf() returning the GQuark.

ugh. Clever, but *ugh*.

> Also, I added Error.prototype.matches(), allowing for a final API like:
> catch(e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)), which is
> neat, IMHO

What's wrong with g_error_matches() ?
Comment 36 Giovanni Campagna 2012-06-07 18:13:01 UTC
(In reply to comment #35)
> (In reply to comment #34)
> [...]
> 
> What's wrong with g_error_matches() ?

Nothing, but if you get a TypeError or a ReferenceError, you want that to pass through and be logged, and not be replaced by the new TypeError caused by calling a non-existing function.
In fact, my Error.prototype.matches() always returns false. You need the real one to do the actual work.
Comment 37 Jasper St. Pierre (not reading bugmail) 2012-06-07 18:16:51 UTC
Oh, gotcha. Again, a bit disgusting.
Comment 38 Jasper St. Pierre (not reading bugmail) 2012-06-07 18:18:24 UTC
Review of attachment 215858 [details] [review]:

This looks fine.
Comment 39 Giovanni Campagna 2012-06-07 18:42:02 UTC
Attachment 208909 [details] pushed as 52e921d - GError: add stack, fileName and lineNumber
Attachment 215858 [details] pushed as 2ab1b3f - Introduce special marshalling for GErrors