util.error

The util.error library implements a common object for passing around errors.

local errors = require "util.error".init(module.name, {
        ["invalid-room-name"] = { code = 400, text = "Invalid room name" }; 
        ["room-not-found"] = { code = 404, text = "Room not found" };
        ["invalid-data"] = { code = 400, text = "Invalid data" };
        ["invalid-json"] = { code = 400, text = "Invalid JSON" };
});

function foo(json)
    if json == invalid then
        return nil, errors.new("invalid-json", { request = request });
    end
end

Overview

Error objects

Every time an error occurs, an error object is created. Error objects are typically “thrown” using Lua’s built-in error() function, or they are returned from a function (e.g. return nil, err), or possibly a promise may be rejected with an error object (reject(err)).

Each error object has a unique id. This can be e.g. reported to the user, and allow an admin to look up the specific occurrence of the error in the log files. An object also has an attached ‘context’ table, which may contain additional information that helps to diagnose the error.

The following fields are present on error objects:

  • instance_id (string)
  • source (string, optional)
  • context (table)
  • type (string)
  • condition (string)
  • text (string, optional)
  • code (number, optional)
  • extra (table, optional)

For definitions of ‘type’, ‘condition’, ‘text’ and ‘code’ see the ‘Error classes’ section. For a description of the ‘context’ field, see the ‘Error contexts’ section.

The ‘instance_id’ is a short random string to identify this specific occurrence of the error. ‘source’ is an optional human-readable string identifying the module or component that created the error object.

The ‘extra’ table may contain additional properties of the error that doesn’t fit anywhere else.

Error contexts

The error context is an additional table that can be used to attach certain metadata and objects to an error instance.

All fields in the context are optional, and all code should be prepared to handle their omission.

The following fields are currently defined:

  • actor (human-readable string identifying the entity that issued the action that caused the error to happen, e.g. IP or JID)
  • by (identity of the entity that issues the error)
  • request (HTTP request object)
  • stanza (util.stanza object)
  • session (util.session object)
  • wrapped_error (string or any other type - a “legacy” error value that has been caught and wrapped in an error object)
  • traceback (string)

Error classes

Where possible, an error object should be derived from a predefined “error class”. These are typically collected in an error “registry”. Each error class provides information about a certain kind of error that can happen, and this information can be used to convert the error to e.g an XMPP error stanza or an appropriate HTTP response as needed.

You can think of an error class as a “template” for new errors of a certain type.

The possible fields of an error class are described in the following section.

Error class fields

These mostly correspond to XMPP stanza errors.

type
Which basic kind of error is. Defaults to cancel. Other common values are auth, modify or wait.
condition
The specific error condition. Defaults to undefined-condition.
text
Human-readable textual description of what happened. Often shown to users, so keep that in mind.
code
Numeric error code. Defaults to 500. Used e.g. in HTTP.

Extras

Certain errors may need additional properties that don’t fit in any of the previously mentioned fields. These can be put into the extra table.

uri
Counterpart to the URI carried in the <gone> stanza error.
Application-Specific Conditions

The XMPP Core specification allows stanza errors to carry Application-Specific Conditions for e.g. PubSub-specific errors. These are represented in the extra table of error objects as a tag field or as a tuple of namespace and condition.

Error registries

An error registry is simply a mapping of predefined values to error classes.

local my_error_registry = {
  ["my-error-type"] = { type = "cancel", condition = "unexpected-request", text = "This is a custom error" };
  ["my-other-error-type"] = { type = "auth", condition = "forbidden", text = "This is another custom error" };
}

local errors = require "util.error".init("my_module_name", my_error_registry);

-- Throw a new error of the class 'my-error-type' with a context identifying
-- 'me@example.com' as the entity that caused the error
error(errors.new("my-error-type", { actor = "me@example.com" }));

API

new(error_class, context, registry, source)

The base function to create a new error object. For convenience it is usually easier to use the init() method described above.

If registry is not provided and/or error_class does not map to a value defined in the registry, and the error_class value is a table, it is assumed to be an error class object, e.g.:

new({ condition = "bad-request" }, { session = some_session })

If there is no registry provided, the error_class key is not found in the registry, and error_class is not a table, it is ignored.

init(source, registry)

This function allows you to set a custom source and/or registry for created errors. It returns an object with a .new(error_class, context) method, which behaves identically to the new() function described above, minus the registry and source parameters.

Compact registry format

If typing type, condition and text repeatedly gets tedious, there is also a compact form of the registry:

local my_errors = require"util.error".init(module.name, {
    bork = {"cancel", "internal-server-error", "something went wrong"};
})

This format only works with .init().

Namespaced error conditions

When using Application-Specific Conditions the namespace can be given as a second argument between the module name and the registry:

local nserr = require"util.error".init(module.name, "urn:example:error:ns", {
    bork = {"cancel", "internal-server-error", "something went wrong", "it-went-sideways"}
})