Skip to main content

Migrating to 0.7

0.7 makes D-Bus errors real Error objects. That is the whole release — nothing else in the API changes.

RELEASE_PLAN.md calls this release "1.0". It ships as 0.7.0 instead: under semver a 0.x minor is already the breaking bump, and 1.0.0 is a statement about stability that is worth making deliberately rather than as a side effect of the first break in the series. Nothing about the content changed; the rest of the plan's sequence still holds.

If you followed the DBUS_DEP0004 guidance in 0.6 and read err.message and err.dbusName, you need no changes at all. This guide is for everyone else.


What changed

behaviour0.6 and earlier0.7
callback error on a failurethe message body, an arraya DBusError
error with an empty body[]DBusError whose message is the D-Bus error name
err instanceof Errorfalsetrue
the error's argumentserr[0], err[1], …err.body[0], err.body[1], …
JSON.stringify(err)["something failed"]{} — as for any Error
missing interfacegetInterface(null, undefined)rejects with UnknownInterfaceError
obj.as('not.an.iface')undefinedthrows UnknownInterfaceError
connection dies mid-callpending callbacks dropped, silentlyall fail with ConnectionClosedError
call made after it diedwarning on stderr, callback never runsfails with ConnectionClosedError

The promise API is unaffected: it has rejected with a DBusError since 0.6, when it was introduced. Timeouts (TimeoutError) and aborts (AbortError) were already Errors in 0.6 and are unchanged.

Also fixed in passing: the error classes are now actually exported. index.d.ts declared DBusError, TimeoutError and AbortError from 0.6, but index.js never exported them, so err instanceof DBusError could not be written at runtime.


Updating your code

Reading the message

bus.invoke(msg, (err, result) => {
- if (err) return reject(new Error(err[0]));
+ if (err) return reject(err);
});
bus.invoke(msg, (err, result) => {
- if (err) console.error('call failed:', err[0]);
+ if (err) console.error('call failed:', err.message);
});

Switching on the error

Unchanged from 0.6 — this is what the forward-compatible properties were for:

bus.invoke(msg, err => {
if (err?.dbusName === 'org.freedesktop.DBus.Error.ServiceUnknown') {
// ...
}
});

Reading the error's other arguments

Most D-Bus errors carry a single string, which is now err.message. If the service sends more than one, the whole body is still there:

- const [text, code] = err;
+ const [text, code] = err.body;

Logging

An Error does not serialise to JSON the way an array did. If you log structured JSON, say what you want explicitly:

- logger.error({ err });
+ logger.error({ err: { message: err.message, dbusName: err.dbusName } });

Most logging libraries (pino, bunyan, winston) already special-case Error instances and will do something more useful than they did with the array.

Handling a dead connection

Previously a connection that dropped left every in-flight call hanging forever — #39. Those now fail, so code that was silently stuck starts reporting:

try {
await iface.SlowCall();
} catch (err) {
if (err.code === 'ECONNCLOSED') {
// reconnect, retry, give up -- but at least you find out
}
}

This is the one change that can make previously "working" (hanging) code start throwing. That is the point of it, but it is worth grepping for calls whose rejection you never handled.


The codemod

For the mechanical part:

npx dbus-native codemod errors-to-error-objects src/

Add --dry to see the diff without writing anything. It applies every rewrite above — err[0]err.message, err[n]err.body[n], new Error(err[0])err, and array destructuring → err.body.

It only rewrites callbacks it can prove are D-Bus callbacks — a function argument to invoke, getInterface, addMatch, getId and the rest of the bus surface. Everything else it leaves alone and reports:

src/net.js:22 DBUS_DEP0004 err[0] on an error this codemod could not
attribute to a d-bus call -- review by hand

The big category there is proxy method calls — iface.Echo('x', cb) — where the member name is the remote method and could be anything. There is no call-site shape to match on, so those are yours to check. A codemod that guesses wrong in an error path is worse than one that does nothing.

It also respects shadowing: a nested function (err) {…} inside a D-Bus callback has its own err, and that one is left alone.

jscodeshift is not a dependency of this package. If your project does not already have it, the command runs it through npx.


The escape hatch

If you cannot migrate a call site yet:

const { toClassicError } = require('dbus-native/compat');

bus.invoke(msg, err => {
const classic = toClassicError(err); // the pre-0.7 array, properties and all
});

Deliberately a subpath import rather than a client option. An option would be a mode — invisible at the call site and inherited by code that never asked for it. An import is greppable, obviously temporary, and deleting it is one line.

toClassicError only converts errors that came from an error reply. A timeout, an abort or a closed connection never had an array form, so those are returned unchanged.


Issues this closes

#39, #178, #207, #208, and PR #213.