Skip to content

Common Scripting Pitfalls

QIE custom scripts run in a Rhino JavaScript interpreter with direct access to Java objects and QIE's binding APIs. Most of the interop is transparent, but a handful of patterns produce confusing runtime errors. This page collects the ones that come up most often, grouped by whether the trap is in the JavaScript/Rhino layer or in a specific QIE function.

If you hit an error this page does not cover, the Code Wizard reference documents every binding and the Recipes show worked examples of the most common script shapes.

JavaScript / Rhino

These traps come from the Rhino interpreter's handling of Java objects exposed to JavaScript code. They are not QIE-specific, but they bite QIE scripts often because nearly every value returned by a QIE binding is a Java object.

string.length returns a function, not a number

Strings returned by QIE methods (getNode, StringUtils.*, channelCache.getValue, etc.) are Java String instances. On a Java String, length is a method, not a property, so writing someValue.length evaluates to the method object itself, and someValue.length - 1 evaluates to NaN. Passing NaN to a Java method that expects an integer throws:

Cannot convert NaN to java.lang.Integer

Use the StringUtils helper to get the length:

var len = StringUtils.length(someValue);

StringUtils is the recommended way to work with strings in QIE scripts. It is consistent with the rest of the QIE script API and is null-safe (StringUtils.length(null) returns 0, whereas null.length() throws). Calling someValue.length() with parentheses also works on a Java string, but prefer StringUtils.length for consistency and null safety.

To strip trailing whitespace (including \r after a CRLF line split), use StringUtils.trim. It removes all leading and trailing whitespace and is null-safe:

var clean = StringUtils.trim(value);

When you need to remove exactly the last character (regardless of what it is), use a negative end position with StringUtils.substring. It counts back from the end of the string and avoids the length arithmetic entirely:

var trimmed = StringUtils.substring(value, 0, -1);

'literal'.getBytes() fails on a JavaScript string literal

A bare JavaScript string in Rhino is not a Java String, so calling .getBytes(...) on a literal throws:

TypeError: Cannot find function getBytes in object queued

This typically shows up when building a small byte payload to upload, for example, a marker blob, a multipart text part, or a fixed header. Wrap the literal in new java.lang.String(...) to get a real Java String that exposes getBytes:

var markerBytes = new java.lang.String('queued').getBytes('UTF-8');

Strings that came from a QIE binding (source.getNode, channelCache.getValue, etc.) are already Java String instances and do not need the wrap. Only string literals in the script trigger this.

String.replace(...) is an ambiguous overload

Calling someValue.replace(regex, replacement) on a Java String throws:

EvaluatorException: The choice of Java method java.lang.String.replace ... is ambiguous;
candidate methods are:
   class java.lang.String replace(java.lang.CharSequence,java.lang.CharSequence)
   class java.lang.String replace(char,char)

Rhino cannot pick between the two Java overloads when the arguments are a JavaScript regex and a string. Use StringUtils.replace(value, search, replacement) instead. It does a literal substring replacement and has a single, unambiguous signature:

var escaped = StringUtils.replace(value, '"', '\\"');

For JSON-escaping specifically, prefer qie.escapeJson(value) over hand-rolling the substitutions. It handles the full set of JSON escape sequences in one call.

String.split(...) treats its argument as a regex

someValue.split(delimiter) on a Java String interprets delimiter as a regular expression, so HL7-style delimiters with backslashes blow up. The classic case is OBX-5 separated by \.br\:

var report = source.getNode('OBX-5').split('\\.br\\');

throws:

PatternSyntaxException: Unescaped trailing backslash near index 5
\.br\

because the trailing \ in the regex has nothing to escape. Use StringUtils.splitByWholeSeparator(value, separator) for a literal-string split that does not interpret regex metacharacters:

var report = StringUtils.splitByWholeSeparator(source.getNode('OBX-5'), '\\.br\\');

The same pattern applies to any delimiter that contains \, ., [, (, *, +, ?, or |. splitByWholeSeparator is the safer default for QIE scripts.

Inserting a line break inside an HL7 field

A raw newline in a field value breaks the segment: HL7 v2 uses \r as its segment delimiter, so an embedded \n produces malformed HL7 that many parsers reject. HL7's formatted-text data types (FT, TX, ST) define \.br\ as the line-break escape. QIE passes it through as literal content, and downstream viewers, printers, and EHRs that follow the HL7 formatted-text convention render it as a line break. Write it in a JS string as '\\.br\\' (backslashes doubled).

parseInt on a missing cache or variable value returns NaN

channelCache.getValue('key') returns null when the key is not defined. parseInt(null, 10) evaluates to NaN, which then fails the same way as above when passed to a Java method that expects an Integer:

Cannot convert NaN to java.lang.Integer

Always supply a default to channelCache.getValue for values that are parsed as numbers, so a missing key falls back to a string that parseInt can handle:

var port    = parseInt(channelCache.getValue('sftpPort', '22'), 10);
var timeout = parseInt(channelCache.getValue('sftpTimeoutMs', '30000'), 10);

The same applies to messageCache.getValue and qie.getVariable. Both can return null. When the value participates in arithmetic or is passed to a Java method, default it at the lookup or guard with StringUtils.isBlank before parsing.

javax.* reports as undefined in the editor

The JavaScript editor's syntax validator (JSHint) is told about the Java package roots java, com, org, net, and ca, plus the Packages, importPackage, and importClass globals. It is not told about javax. Writing:

var mac = javax.crypto.Mac.getInstance('HmacSHA256');

triggers a 'javax' is not defined validation warning even though Rhino itself can resolve the class at runtime. The fix is to address the class through the Packages global, which the validator does know about:

var mac = Packages.javax.crypto.Mac.getInstance('HmacSHA256');
var key = new Packages.javax.crypto.spec.SecretKeySpec(keyBytes, 'HmacSHA256');

The Packages. prefix works for any Java package, not just javax. It is the official Rhino entry point into the Java type system and is the safest form to use in scripts that call into Java classes outside of java.*. Bare java.io.File, java.util.Date, and so on remain fine because java is in the validator's globals list.

JSON.stringify throws on Java-backed values

When an object appears in a log line as the literal text [object Object], JavaScript's string concatenation has called the object's default toString() instead of showing its contents. The usual fix (replacing qie.debug('obj = ' + obj) with qie.debug('obj = ' + JSON.stringify(obj))) hits the Java-backed-values trap described here on the next run.

JSON.stringify walks its input using JavaScript's own type system. Native JS numbers, strings, booleans, arrays, and objects serialize fine. Values that are actually Java objects, such as java.util.Date returned by channelCache.getDate(...), an entire java.util.Map handed in through a script variable, HashMaps built up in Java-invoked utility calls, do not, and JSON.stringify either throws or emits {}/null where a real value was expected.

Coerce to a native JavaScript value before handing it to JSON.stringify:

// java.util.Date → string via JS coercion.
var lastRunIso = '' + channelCache.getDate('lastRun');

// java.util.Map → plain object via a rebuild step.
var jsCopy = {};
var entries = javaMap.entrySet().iterator();
while (entries.hasNext()) {
    var entry = entries.next();
    jsCopy[entry.getKey()] = '' + entry.getValue();
}

// Now safe to stringify.
var json = JSON.stringify({ lastRun: lastRunIso, values: jsCopy });

For cases where the Java object should be included as-is if it happens to be a supported type, wrap the stringify call in a replacer that catches unknown types:

JSON.stringify(payload, function (key, value) {
    if (value instanceof java.util.Date) {
        return '' + value;
    }
    return value;
});

QIE Functions

These traps come from non-obvious return-value semantics of specific QIE bindings. The function still works as designed, but the documentation does not always make the edge case obvious until you hit it.

qie.listSFTPEndpoint returns leaf file names, not full paths

The array returned by qie.listSFTPEndpoint(...) (and its FTP variants listFTPEndpointNoEncryption, listFTPEndpointExplicitTLS, listFTPEndpointImplicitTLS) contains just the file name of each match, for example report_20260609.txt, not /outbound/metadata/report_20260609.txt. qie.readSFTPFile(...) expects the full remote path. The script must join the source folder and the returned name itself before calling read:

var leafName   = remoteFiles[i];
var remotePath = sftpSourcePath + '/' + leafName;

var downloaded = qie.readSFTPFile(host, port, remotePath, /* ... */);

Passing the bare leaf name to qie.readSFTPFile returns a null result, and the next getNode call fails with Cannot call method "getNode" of null. See Polling SFTP from a Custom Script Receiver for the full pickup pattern.

message.discard() and message.error() inside try/catch are silently swallowed

Wrapping message.discard() or message.error() in a try/catch cancels the call. Both signal the processor by throwing an exception, and the surrounding catch block catches that exception before the processor ever sees it, so the discard or error never takes effect and the message continues down the channel.

Keep these calls outside any try/catch:

var shouldDiscard = false;
try {
   // ... logic that might decide to discard ...
   if (someCondition) {
      shouldDiscard = true;
   }
} catch (err) {
   qie.error('Failed: ' + err);
   throw err;
}

if (shouldDiscard) {
   message.discard();
}

qie.error(...) inside the catch is the logger (writes a log entry at error level) and is safe. Only message.error(...) triggers the processor-level error path that the catch would swallow.

Trailing commas break JSON templates in qie.evaluateTemplate

qie.evaluateTemplate does not parse the rendered output as JSON; it returns whatever string the substitutions produce. A JSON template with a trailing comma inside an object or array is invalid JSON, and any downstream call that does parse the result (for example message.addObjectToJSONArray(...), qie.parseJSONString(...), or a REST destination sending application/json) either fails or (more confusingly) treats the whole output as an escaped string value.

The symptom is a message body that looks right at a glance but appears in downstream logs surrounded by extra quotes and backslash-escaped characters ("{\"mrn\":\"MR001\", ... }") instead of a real JSON object.

Fix by removing the stray comma. A template such as this:

{
    "mrn":     "{PID-3.1}",
    "family":  "{PID-5.1}",
    "given":   "{PID-5.2}",
}

should be:

{
    "mrn":     "{PID-3.1}",
    "family":  "{PID-5.1}",
    "given":   "{PID-5.2}"
}

The same applies to a trailing comma after the last element of a JSON array in a template.