This document captures common practices in designing APIs that fit well into the Web platform as a whole, using WebIDL [[!WEBIDL]].
This document has been developed progressively over time with help from the WebApps WG and the ScriptLib CG.
Over a relatively short period of time the number of different APIs being created for use on the Web has grown at a sustained pace. In working on these interfaces, many in the community discuss the benefits of certain approaches over others and reach agreement as to the preferred solution when facing a given problem.
Keeping track of all these gems is however difficult given the volume of work being carried on in parallel and the sometimes disjointed nature of the groups involved. As a result, it can take a long while and many arguments repeated almost identically before a good solution becomes common.
What's more, it is altogether too rare to garner feedback from developers who are actually using these features in their day-to-day work. Because of this, several APIs have shipped with issues that could have been caught earlier on in the process. This document therefore also endeavours to elicit feedback from developers in order to make it more readily available to API designers.
The goal of this document is to capture such ideas as they appear and accrete them over time so as to constitute a repository of knowledge on this topic. As a guide it does not however attempt to supplant editors' brains in making decisions as to how to design their APIs, and consequently one must keep in mind that the reasoning behind a specific recommendation is often more important than its result. Furthermore, in some cases there may not be a single consensual best approach, and editors will need to understand the tradeoffs involved in order to pick what works for them amongst a set of options.
This section covers the very basics of API creation for the Web platform. If you are familiar with the APIs found there, you might wish to simply skim the titles.
Casing is relatively coherent across the platform, and if you are familiar with JavaScript development it should come to you rather naturally.
Interfaces, dictionaries, exceptions, and callbacks all use “CamelCase”. That is to say no underscores are used and the first letter of every word is in uppercase. The rules when one of those words is an acronym are not necessarily well established — follow your instinct (or try to avoid acronyms).
FascinatingDocument Node SomethingBadHappened XMLHttpRequest
Both attributes and methods use “pascalCase” also known as “interCaps”. Which is to say that the first letter is lowercase, no underscores are used, and all the following logical words in the name start with an uppercase letter.
obj.thisIsAnAttribute obj.simple obj.doSomethingNow() obj.run()
Constants are all uppercase, with logical words separated by underscores. Families of constants tend to have common suffixes. Please note that constants are decreasingly used and that it is likely you will not be having recourse to this construct very much (see Don't Use Numerical Constants).
REALLY_STUPID_ERROR PASTA_SAUCE_TYPE
Events are a bit special and for historical reasons have somewhat more convoluted rules. The interface
that represents a specific event will follow the general rules for interfaces, ending with Event
.
But the event name will be all lowercase and with no underscores. Likewise, the on*
event
attribute will be the event name (all lowercase and glued together) simply prefixed with “on
”.
SomethingHappenedEvent obj.addEventListener("somethinghappened", handler, false); el.onsomethinghappened = handler;
It is not always obvious when one should use an attribute rather than a method. The basic rule is simple: use attributes as often as possible.
Never use getFoo()
/setFoo()
accessor pairs unless it is very clear that these are
operations that have strong effects beyond just changing the property of the object. If you wish your interface
to also be usable in a language other than JavaScript and for which such accessors tend to be needed, the bindings
for that language can naturally generate the accessor pair. Don't do this:
var len = obj.getLength(); arr.setLength(num);
But rather:
var len = obj.length; arr.length = num;
Beyond operations that obviously require a method (that take parameters, return a computed value, carry out an action, etc.) there are specific cases in which what might at first seem like a candidate for being an attribute is best handled as a method:
objA.child = objB
it may be necessary to set objB.parent
to objA
behind the scenes. Such side effects call for a method: objA.setChild(objB)
.
stat
calls are not cheap). In such cases it is best to make the accessor an asynchronous method that is
passed a callback such as someFile.size(function (bytes) { … })
(or, if there are many such properties on the object, have a single call to load them all, for instance
someFile.stat(function (info) { … })
).
The Web platform's runtime model is based on a single-threaded event loop. Because of that, any time a method requires even a little bit of time to run, it blocks the thread, and therefore the execution of anything else on the page — leading to bad user experience.
Example of operations that can take a while to complete include reading from the network, accessing the disk, performing a database search, or waking a sensor up.
Another case in which asynchronous operations are required is for all security entry points. Some operations (e.g. obtaining the device's physical location) can require the user agent to obtain the user's permission. If that were to be done synchronously, the entire application would freeze while the user makes her decision. This is undesirable in terms of user experience, but it also a bad security model. The user needs to have all the time she wants in order to make such decisions and should not feel pressured by a frozen application (people make bad security decisions when they want to get something done).
It can seem tempting to make methods either synchronous or asynchronous at the developer's discretion.
The simple answer to that is: don't. The result can be catastrophic: for instance, while in development
a method that has to interact with a remote resource may return quickly because of light load on the
server and proximity between the developer (who is using a powerful device on a fast network) and the
server, so that the synchronous call may seem fine. But once deployed, it can cause long, very
perceivable freezes for the end-users. The XMLHttpRequest
interface does make that option
available on its open()
method and there is broad consensus that it was a bad decision that
should never be emulated.
A context that is not similarly affected by synchronous issues is that of Web Workers (since they run in a separate conceptual thread from the Web application's user interface). In such cases, it can sometimes be useful to provide a synchronous variant of an asynchronous interface that would be available solely in a Worker context. This is primarily done in order to make development simpler in such contexts and should not be considered required.
Interfaces regularly require constant identifiers. You have a node and you want to know if it's an element or a comment. You have a message and you want to know if it's email or SMS. That much makes sense. What does not make sense is insisting on naming these things with numbers when we could name them with, you know, names.
There are several variants in this bad practice. First, is the lovingly terse approach:
if (foo.type === 17) doSomething();
Then there is the comment you have to paste every time:
// check if this of the fungible type if (foo.type === 17) doSomething();
Or we can have the uselessly long variant:
if (foo.type === FooInterface.FUNGIBLE_TYPE) doSomething();
And there's the meh option:
var isFungible = FooInterface.FUNGIBLE_TYPE; // ... if (foo.type === isFungible) doSomething();
None of the above is as simple, terse, and self-documenting as:
if (foo.type === "fungible") doSomething();
We're not in the 1970s, standardising UNIX system calls. Strings are lightweight enough, and readable. There is no need to define string constants to support these: just the plain strings, defined as such in specification prose, work. In some languages, this can lose static verification, but in JavaScript it makes no difference: if you misspell the the string you might have equally misspelt the constant, with the same failure.
Additionally, interfaces that don't start out by listing a dozen (useless) constants are more readable. The only downside to doing away with numerical constants is the negative impact it may on occasion have on feature detection. It would however be better to rely on other aspects that enable feature detection, since these constants can easily be added for a half-baked feature (and frequently have been).
If you are developing functionality for use inside of a Web runtime (but not a browser) that is likely to both share code with existing Web projects (typically, libraries like jQuery) and that also plans to evolve and integrate improvements to the Web platform on a regular basis, then you should make sure that the interfaces and methods you add cannot conflict with ones added to the core platform.
For methods and attributes that extend existing interfaces, you should rely on vendor prefixing, i.e. simply prefixing your additions with a short string that is your company's or project's name.
xhr.acmePassiveFTP(); document.acmeDigitalSignature;
For interfaces that you add, rather than prefixing all of their names which can get tedious, it can be simpler to just make them available under your own namespaced object.
var toaster = new acme.Toaster();
WebIDL [[WEBIDL]] is a powerful and flexible schema language for APIs. It is a useful foundation in ensuring that APIs can be defined interoperably without each and everyone of them needing to repeat all manners of behavioural specifics.
It can however be somewhat daunting to use at times, and the intricacies of the platform mean that it has some dark or at least non-obvious corners. This chapter endeavours to help clarify at least enough of it that you will feel comfortable making use of it.
While designing Web APIs, you might encounter situations where you need to define an interface
that has no method. That is, for certain requirements, you need a method to define a property
bag object that would be expressed as an Object
object or as an object literal in
JavaScript implementation. This is where the dictionaries come into play.
This section presents a guideline to use dictionaries at the right place in your specification without any common mistake. First, in the "When to use" section, we show the typical cases you can make use of dictionaries. Second, in the "How to use" section, we provide a list of features that you can check in order not to make a common mistake with dictionary usage.
Property bag objects are commonly used to define optional parameters for certain methods of given interfaces. Also, they can be used to define necessary data structures and data formats in a given application context. Here are the representative examples:
Case 1: Arguments to Interface Method When designing interfaces using WebIDL, you can use dictionaries to define a property bag object that carries multiple arguments to its method. In many practices, the methods take dictionary value as an optional argument although it is not mandatory.
// WebIDL partial interface IDBDatabaseSync { IDBObjectStoreSync createObjectStore (DOMString name, optional IDBObjectStoreParameters optionalParameters); }; dictionary IDBObjectStoreParameters { DOMString? keyPath = null; boolean autoIncrement = false; }; // JavaScript var parameters = { keyPath: "id" , autoIncrement: true }; trans.db.createObjectStore("Contact", parameters);
More examples in published specifications: http://www.w3.org/TR/FileAPI/#creating-revoking
Case 2: Arguments to Constructor Using dictionaries, you can also define a constructor arguments to the given interfaces. This allows the developers create and pass an object or an object literal to the constructor of the JavaScript objects.
// WebIDL [Constructor(IntentParameters params), Constructor(DOMString action, DOMString type, optional any data, optional sequence<Transferable> transferList)] interface Intent { readonly attribute DOMString action; readonly attribute DOMString type; readonly attribute any data; readonly attribute MessagePort[] ports; readonly attribute any extras; void postResult (any data, optional sequence<Transferable> transferable); void postFailure (any data); }; dictionary IntentParameters { DOMString action; DOMString type; any data; sequence<Transferable> transfer; Object extras; URL service; sequence<URL> suggestions; }; // JavaScript var intent = new Intent({ action: "http://intents.w3.org/pick", type: "image/png", extras: { multiple: true, count: 5 }, service: "http://example.com/pick-image" });
More examples in published specifications: http://www.w3.org/TR/2012/WD-dom-20120405/#interface-event or http://www.w3.org/TR/FileAPI/#blob
Case 3: User object A dictionary is normatively defined as an associative array data type with a fixed, ordered set of key-value pairs. That is, you can use a dictionary to define a custom data type carrying a set of properties as an object format used in the given application context.
// WebIDL partial dictionary Media { MediaContent content = {}; DOMString title; DOMString description; DOMString type = "*.*"; DOMString author; DOMString path; float size; sequence<DOMString> tags; Date publishDate; Resolution resolution; }; // JavaScript var mediaSequence = []; var mediaObject = { content: { uri: "http://example.com/images/kitten.png" }, title: "", description: "", type: "image/png", author: "", path: "/local/path/to/png/kitten.png", size: 1000000, tags: ["tom and jerry", "miyau"], publishedDate: "", resolution: { width: 1920, height: 1080 } }; mediaSequence.push(mediaObject); intent.postResult(mediaSequence);
More examples in published specifications: http://www.w3.org/TR/2012/WD-gallery-20120712/#the-media-dictionary or http://www.w3.org/TR/2012/WD-contacts-api-20120712/#the-contact-dictionary
Dictionaries are always passed by value. Hence, when you define a dictionary member that should carry an array of certain value, use sequence type rather than Array type. Sequences are always passed by value while arrays are passed by reference.
dictionary IntentParameters { DOMString action; DOMString type; any data; sequence<Transferable> transfer; Object extras; URL service; sequence<URL> suggestions; };
In addition, there is another check point where sequence type should be used rather than array type. WebIDL describes that the dictionary type must not be used as a type of element of an array. Hence, when your dictionary has to carry an array of the dictionary values as its member, use sequence type.
dictionary DoNotUseLikeThis { IntentParameters[] params; }; dictionary UseLikeThis { sequence<IntentParameters> params; };
Dictionaries support inheritance as interface does. A dictionary must not be defined having a cycle in its hierarchy.
dictionary Novel : Book { // dictionary-members... }; dictionary Book : Novel { // dictionary-members... };
On a given dictionary value, the presence of each dictionary member is optional. Therefore, in order to indicate that certain members are required fields in your specification, use default values. Dictionary members with default values are always considered to be present.
dictionary PropertyBag { DOMString iAmRequired = "default"; DOMString iAmOptional; };
In the example, since iAmRequired
has default value, it is always
considered as present when the dictionary value is passed. On the other hand, when
iAmOptional
is missing, it is regarded as absent.
It is one of common mistakes that a dictionary type is used with attribute field in interface definition. In face, dictionaries must not be used as the type of attribute, constant or exception fields.
interface HasWrongUsage { readonly attribute IAmDictionary dict; };
To correct it, use type any or object and describe in prose that the attribute takes the type of the dictionary.
interface HasRightUsage { readonly attribute any dict; };
In your spec, write "dict is of type IAmDictionary dictionary as defined in [REFERENCE]".
Do not use dictionary type as inner type of a nullable type. By definition in WebIDL dictionary,
valued with null
, is regarded to be an empty dictionary, which is not null
.
(It can even have the value of members defined in their default values.)
dictionary HasWrongUsage { IAmDictionary? dict; }; dictionary HasRightUsage { IAmDictionary dict; };
As members of a dictionary are optional by definition, the developer who reads your specification can omit the member that she intends to be not present in the given application context.
As with interfaces, the IDL for dictionaries can be split into multiple parts by using partial dictionary definitions.
dictionary SomeDictionary { // dictionary-members… }; partial dictionary SomeDictionary { // dictionary-members… };
Only the following two extended attributes are applicable to dictionary
members: [Clamp]
, [EnforceRange]
.
Interfaces are the core work horse of WebIDL as they are the means through which functionality is
exposed. They are introduced with the interface
keyword, have a name, and a body that
contains their definition in terms of constants, attributes, and methods (known as “operations” in
WebIDL). A typical interface looks like this:
interface BeerBottle { // body goes here };
Without modifiers, an interface will become available in the global context as an object with the same name.
Interfaces can inherit from other interfaces, in which case they acquire their properties. Inheritance is specified like this:
interface BeerBottle : Bottle { // body }; interface Spork : Spoon, Fork { // body };
As in the last example above, an interface can inherit from any number of other interfaces. This practice however is discouraged if it can be avoided as it entails more complex resolution rules and can be difficult to implement in some contexts. Multiple inheritance is often a sign that the API design should be revisited and can be simplified.
It is common to have to add functionality to an existing interface, or to wish to define a single interface spread over multiple locations. This can happen primarily for two reasons:
Navigator
). It's an important aspect of the Web platform's core extensibility.
In both cases there is a need to extend an existing interface, and inheritance is not the solution since one effectively wishes to mix functionality into the existing interface, and not create a new one.
A construct that was previously used for this but is no longer recommended was
to create the extending interface with [NoInterfaceObject]
and then state
that the existing interface implements
the extending one. This is
exemplified below:
// we wish to add findUnicorns() to Navigator [NoInterfaceObject] interface UnicornNavigator { Unicorns findUnicorns (DOMString name); }; Navigator implements UnicornNavigator; // you can now call navigator.findUnicorns("tab");
This is clunky however, and no longer necessary. It can still be found in some drafts but should not be copied.
The proper way of implementing this feature is to use partial
interfaces. These
are just like regular interfaces, except that they are defined to specify simply a fragment of
the whole, and can therefore add to existing interfaces that may be defined anywhere else.
The previous example can therefore best be rewritten as:
partial interface Navigator { Unicorns findUnicorns (DOMString name); };
If you have read WebIDL as found in many specifications, you will have noticed that there are special constructs that can decorate existing ones in order to enhance them. These are called “extended attributes” and their goal is not to change the WebIDL itself — which is to say that they don't modify the abstract schema described by WebIDL — but to influence how the concrete language bindings operate.
In this section we look at several extended attributes that have a bearing on how interfaces are bound in JavaScript.
By default, when an interface is bound in JavaScript, an interface object for it is available
globally but it is not constructible. That is to say that it can be used for instance for
instanceof
testing, or that it can expose some members such as static methods,
static attributes, or constants but you can't construct it with new
. Try out
for instance in your JavaScript console:
new Node() // TypeError: Node is not a constructor
Therefore, in order to use a constructor in JavaScript, you will need to use the [Constructor]
extended attribute. [Constructor]
can optionally take parameters (not giving it parentheses is
the same as giving it an empty set) and you can specify multiple ones.
An example might make this clearer:
// new Unicorn(); [Constructor] // the same as [Constructor()] interface Unicorn { // ... }; // new Unicorn(); // new Unicorn("tab"); // new Unicorn(5, 82); [Constructor, Constructor(DOMString name), Constructor(unsigned long wingspan, unsigned long age)] interface Unicorn { // ... };
At times it is desirable to have the constructor for an interface have a different name from that
of the interface. For such cases, use the [NamedConstructor]
extended attribute. It
works in the exact same way as [Constructor]
, except that it also accepts an identifier
that specifies the name to use for the constructor. An example will show the syntax:
// new Rose(); // new Rose("pink"); [NamedConstructor=Rose, NamedConstructor=Rose(DOMString colour)] interface RosoideaeRosa { // ... };
Another useful extended attribute for interfaces is [ArrayClass]
. It comes in handy when
it is necessary to define an interface that can behave just like a real JavaScript array. It is meant
to be used with the getter
, setter
, creator
, and deleter
keywords for indexed properties.
These keywords are rather straightforward:
getter
defines what happens when a value is fetched from the array, so that for instance
the following can work: var carrots = shoppingList[7];
.
setter
and creator
is the reverse and defines what happens when a value is
set in the array (on an existing entry or a newly minted one — for arrays you usually want to use the
same operation for setter
and creator
), for instance with the following:
shoppingList[0] = "beer";
or
shoppingList.push("unicorns");
deleter
defines what happens when a value is removed from the array, for instance
with: shoppingList.pop();
.
Assembling this all together looks like this:
[ArrayClass] interface ShoppingList { attribute unsigned long length; getter object getShoppingItem (unsigned long index); // note how both creator and setter are used here creator setter object setShoppingItem (unsigned long index, object item); deleter void removeShoppingItem (unsigned long index); };
The above give you a ShoppingList
interface. You could use it as follows:
var list = obj.findShoppingListFor("robin"); list = list.concat("beer tea basil ham cat-food".split(" ")); list.forEach(function (item) { remindUser("Shop for " + item); }); list.pop(); // bad cat console.log("Getting " + list.join("\n"));
There have been many excessive uses of [NoInterfaceObject]
— unless you are absolutely
certain that it is what you want, you should avoid it. The odds are good that what you are looking
for is either a partial interface or a dictionary.
In practice it should only be used for abstract interfaces that enrich a concrete one through the
implements
statement.
Constants are a simple construct that does exactly what it say on the tin. It creates a member of a given type on
an interface (or an exception) the value of which cannot be changed, introduced with the const
keyword.
Constants are limited in the values they can accept. They can take numerical values (including the likes of
Infinity
and NaN
), booleans, or null
(for cases in which you might forget
what the value of null
is). Examples tell you all you need to know about the syntax:
interface Something { const short ANSWER = 42; const boolean WRONG = false; const unsigned long BIG_NUMBER = Infinity; };
While they were previously used rather commonly, there are decreasing reasons to want to resort to constants on interfaces. Before using them, be sure to be aware of Enums and Don't Use Numerical Constants.
An enumeration is used to specify a set of strings that are acceptable, it is typically used to define options that are restricted to a limited vocabulary.
Enumerations are defined with the enum
keyword, a type name, and then a list of
strings (with no duplicates) inside curly brackets. The name can then be used where type
identifiers usually appear.
enum Unit { "m", "cm", "mm" }; interface Unicorn { void setHornSize (unsigned long length, Unit unit); }; // This works // unicorn.setHornSize(42, "cm"); // This blows up as well it should // unicorn.setHornSize(7, "in");
When an attribute is defined to be an enumeration type, then assigning a value to it that is not in the enumerated set simply does nothing.
Attributes are a fundamental building block of interfaces. They are used to specify data fields of
a given type. An attribute is introduced with the attribute
keyword.
They can be read-only in which case they cannot be assigned to, or read-write. It is important to note that if the value of a read-only attribute is not atomic but has attributes of its own, those are not made read-only as well — they keep operating just as specified by that type. An example:
interface Name { attribute DOMString givenName; attribute DOMString familyName; }; interface Person { readonly attribute Name fullName; };
In the above definition, it is impossible to assign a new value of the fullName
field
of Person
. So you could not do the following: billie.name = someName;
.
But the fields of the Name
object itself stay just as re-write so that they can still
be set, as in billie.name.familyName = "The Cat";
.
An attribute can also be said to be static
, in which case it is an attribute of the
interface rather than being associated with the object. Static attributes are rarely good ideas
and can tend to indicate a bad design.
// set the debug flag globally interface Runtime { static attribute boolean debug; };
A specific behaviour can be added to attributes to define how a given object is turned into a string,
using the stringifier
modifier. When a object is cast to a string, if a stringifier
has been specified (there can be only one) then the string produced will be the value of that
attribute. Naturally, that attribute has to be of type DOMString
.
// another Person interface [Constructor(DOMString name, short age)] interface Unicorn { stringifier attribute DOMString fullName; attribute short age; };
This can then be used with the following code:
var uni = new Unicorn("Tab", 42); document.getElementById("output-unicorn").textContent = uni; // that element now has content "Tab";
Methods are the workhorses of WebIDL. In the specification they are known as “operations”, but since everyone calls them methods we have decided to stick to this term as it will be more readily understood. They are used to define behaviour.
Methods have a return type, and a list of arguments which can be precisely specified. As for attributes,
a method can also be said to be static
, in which case they become available on the
interface object directly.
interface KittenFactory { static Kitten makeMeANewKitten (DOMString name); }; // var billie = KittenFactory.makeMeANewKitten("Billie The Cat");
The return type can be any type or void
, which indicates that nothing is returned.
The most complex part of a method's definition is its arguments list. It can contain any number of argument definitions, each of which has a type and a name; all of them separated by commas.
Note that it used to be that arguments were all preceded by the in
keyword, which was
a leftover from OMG IDL. Since Web specifications make no use of the out
and inout
argument styles of the aptly named OMG specification, the in
keyword has been dropped and
must not be used.
A basic method therefore looks like this:
interface Unicorn { void gallop (unsigned long speed, Pattern rhythm); AudioStream speak (); };
The arguments can be modified in a number of ways. First, an argument can be marked as
optional
in which case it can be omitted when the method is invoked. If so,
it should be noted that all the arguments that are positioned after it in the list will
also be considered optional.
enum Direction { "left", "right" }; interface Dahut { void turnAround (optional Direction whichWay, short degrees); }; // I can call someDahut.turnAround()
Note that in the above it is not indicated that the degrees
argument is optional even
though it implicitly is. For readability purposes, it is best to always indicate that an argument
is optional even when it implicitly is as someone scanning the method definition quickly from the
end might not notice it.
A problem with the above method definition is also that while my dahut can only turn left or right, there is no indication of what happens if I omit the argument. Is it right or left? Or random? This can be remedied in specification prose but it is better to specify a default directly in the WebIDL, as follows:
enum Direction { "left", "right" }; interface Dahut { void turnAround (optional Direction whichWay = "left", short degrees); }; // when I call someDahut.turnAround(), it now turns left
An argument can also be variadic. This indicates that that argument can be repeated zero or
more times when the method is called. This can only be done if it is the last argument in the arguments
list. Variadic arguments are specified by appending "...
" to the type.
interface Restaurant { DeferredFood orderFood (DOMString... dishName); }; // I can now call // resto.orderFood("spam", "spam", "spam", "spam", "spam", "spam", "spam", "spam", "spam");
WebIDL makes it possible to use overloading of methods in interfaces so that there can be more than one method with a given name that takes a different set of arguments. This happens simply by specifying the method more than once.
Note that method overloading is rarely needed and should usually be avoided unless there is no better solution, if only because the restrictions on how it can be used and the resolution rules can be quite complex. A lot of the cases in which one may think that overloading may be required are actually better addressed with optional arguments, variadic arguments, or at times union types.
Union types are used where types can be used, but instead of specifying just one type they
indicated that the values can be of several different types. They are specified between parentheses,
with each type separated by the or
keyword.
interface Party { void bookDate ((Date or unsigned long or DOMString) date); }; // I can now call with... // a JS Date object // party.bookDate(new Date(…)); // time since epoch // party.bookDate(1337998907987); // a magic string // party.bookDate("tomorrow");
This can be useful for return values as well:
interface Cryptozoology { (Dahut or Unicorn) findAnimal (); };
Exceptions are special classes that can be used to be thrown. They are very limited interfaces that can only define constants, and a limited kind of attribute (known as “fields”) that are defined solely by a type and a name.
All exceptions automatically get a message
and a name
field (for which
the specification should provide values), such that those need not be explicitly specified.
It is strongly recommended that new specifications do not define
new exceptions, but rather reuse DOMException
while providing a specific name
for it.
In addition to DOMException
, a number of predefined exceptions are also available that
can be reused by specifications. This list is: Error
, EvalError
, RangeError
,
ReferenceError
, SyntaxError
, TypeError
and URIError
(these correspond to their JavaScript equivalents as defined in [[ECMA-262]]).
Due to these constraints on usage, we have decided not to delve deeply into exceptions in this section.
It is very common for asynchronous methods in Web APIs to accept callbacks, which are functions
that can be called with specific arguments when called back. These are specified with the
callback
keyword, a name, a return type, and an arguments list which is similar
to that used for a method.
Specifying callbacks is simple:
// defining callbacks callback LandingOk = void (unsigned long landingSpeed); callback LandingFail = void (DOMString gravity, short casualties); // using callbacks interface FlyingUnicorn { void land (LandingOk, LandingFail); }; // in code myFlyingUnicorn.land( function (speed) { console.log("Successfully landed at " + speed + "kph!"); } , function (severity, dead) { console.log("Crashed landing. Damage level: " + severity + ". Dead elves: " + dead); } );
I admit to still being largely mystified by this. Recent discussion on the mailing list does not at all dispel that feeling.
The features of WebIDL listed in this section have been specified for the sole purpose of describing legacy APIs such as are found in “DOM 0”. It is particularly important that you do not use them unless you are trying to document one such API — they must not be used for any kind of new work. The list includes:
Get an actual list.
Beyond understanding the basics of WebIDL there are common and useful idiomatic constructs that can usefully be shared. This section endeavours to progressively accrue some more, so please send yours in!
We had talked of Yehuda writing this section. I will see if I can hunt him down.
Asynchronous method calls require a point of reentry in the form of a callback. There are multiple ways of specifying these, none of which is consensually consider the one and only approach.
One is success/error callback functions, as used in [[GEOLOCATION-API]]. It takes the shape of a method call that accepts a callback function for success followed by one for errors. There may be arguments before the success callback, and the error callback is usually optional.
navigator.petKitten( function() { console.log("kitten is purring"); }, function(err) { console.log("kitten hates you, because: " + err); } );
Downsides of this approach are that it is a little more verbose and does not encourage error handling. It also lacks in extensibility since adding new arguments to the method will typically require placing them after the optional error handler — making them de facto optional. Likewise, extending it to support not just callbacks for success and error but also for other situations that may warrant reporting information back (e.g. progress being made) is clumsy at best. Since it is only used in [[GEOLOCATION-API]] it does not appear to be a required pattern for consistency.
Another approach is to define new DOM Events that are called when an underlying system changes.
window.addEventListener("kittenpurr", function() { console.log("kitten is purring"); }, false);
On one hand, most developers are quite familiar with DOM Events, and they make it easy to have several handlers for a given event.
On the other hand, using DOM Events for listening to systems that need specific activation (e.g. powering up) are usually not a good idea: addEventListener
is supposed to be free from side effects, and associating activation code with it breaks that promise. The [[DEVICE-ORIENTATION]] specification is as such an anti-pattern that should not be replicated. See some discussions on side effects in addEventListener.
Another is returning an EventTarget object, as pioneered by [[XMLHTTPREQUEST]]. This consists in opening a request that returns an object on which event handlers can be registered, then starting that request with a specific call.
var kitpet = navigator.openKittenPetting(); kitpet.onpurr = function () { console.log("kitten is purring"); }; kitpet.ondrool = function () { console.log("kitten is now drooling"); }; kitpet.onviciousbite = function () { console.log("kitten bit your hand off"); }; kitpet.start();
The approach is the most verbose and does not encourage error handling, but it does have the advantages of being trivially extended for greater feedback, and of being familiar to users of [[XMLHTTPREQUEST]] (and now [[INDEXEDDB]]). It can, however, feel rather overkill if extensions for additional events does not seem likely.
Then there are NodeJS style callbacks. For every asynchronous operation, every callback takes
as first parameter an error object which is null
when the operation was a success, followed
by as many pieces of information as are needed to capture the success.
navigator.petKitten(function (err, state) { if (err) console.log("kitten mauled your face with extreme prejudice"); else console.log("kittem manifest joy by " + state); });
The primary value in this approach is that it puts errors "right there" where they are likely to be handled. Its consistency is also very helpful in creating chains of callbacks, a feature that proves precious when multiple callbacks can become nested. The major downside of this approach is that it is currently not used in the rest of stack and therefore probably only familiar to NodeJS developers (which granted represent a fast-growing proportion of the general web developer population).
Finally there are promises. These are similar to returning an EventTarget object but more generic and use a slightly different style.
var promise = navigator.petKitten(); promise.then(function () { console.log("kitten is purring"); }).fail(function () { console.log("kitten bit your hand off"); }).run();
There is elegance and flexibility in promises, but to date they are used in no Web specification.
This ought to be relatively straightforward.
Should we have a section on specifying with Web Intents?
A number of other sources provide information that can be useful in writing specifications. This list is by no means complete, suggestions are welcome.
Anne maintains a document called How To Spec on how to specify certain features of the platform. Some parts of it are tailored to the specific markup that is used when processing specifications with Anolis, but the general advice is sound.
Many thanks to the following people, in no particular order: Cameron McCormack, Marcos Càceres, Andreas Gal, Rick Waldron.