Error Handling and Retry
Interfaces fail. What separates a maintainable interface from a fragile one is deciding, in advance, which failures stop the feed and which ones do not.
Classify the failure first
| Category | Example | Retry helps? | Response |
|---|---|---|---|
| Transient | Timeout, connection reset, receiver restarting | Yes | Retry with backoff |
| Data | Missing required field, malformed payload | No | Reject the message; the sender must fix it |
| Configuration | Wrong port, bad credentials, missing certificate | No | Fix the configuration |
| Business rejection | Receiver returns a negative ACK or a 409 | Depends | Follow the receiver's contract |
Retrying a data error just fails repeatedly at a slower rate. Refusing to retry a transient error turns a two-second network blip into a manual restart.
Raising an error in a script
Use error() when the message cannot be processed correctly:
local mrn = Msg.PID[3][1][1]:value()
if mrn == nil or mrn == "" then
error("PID-3 patient identifier is missing")
end
Name the field, not its value. Error text is stored and searchable, so a value in an error message becomes a patient identifier in a log.
| Write | Not |
|---|---|
error("PID-3 patient identifier is missing") | error("bad MRN: " .. mrn) |
error("receiver returned " .. resp.code) | error("failed sending " .. Data) |
Filtering is not an error
Returning without pushing drops a message deliberately. That is normal operation:
if MsgType ~= "ADT" then
print("Skipping message type " .. tostring(MsgType))
return -- not an error
end
Print a line when you do. Otherwise a working filter and a broken node look identical to whoever asks why nothing arrived.
Never swallow a failure
The most damaging pattern in an interface is treating a failed delivery as success:
-- Wrong: the message is lost and nobody knows
local resp = linkiir.link.web.post{ url = Url, body = Data }
-- Right: check both the transport and the response
local resp, err = linkiir.link.web.post{ url = Url, body = Data }
if not resp then
error("delivery failed: " .. err.message)
end
if resp.code >= 400 then
error("receiver returned " .. resp.code)
end
resp being present means the request completed, not that the receiver accepted it. Check the status code too.
Stopping versus continuing
When a script raises an error, the node reports the failure and stops processing. Unprocessed messages wait in the queue.
That is the safe default for clinical interfaces, and it is worth understanding why: stopping is not data loss. The queue is durable, order is preserved, and the failure is impossible to miss. A node that silently continues past errors can discard a day of data before anyone notices.
Where individual bad records are genuinely expected and must not halt a feed, handle them in the script rather than letting them raise:
local linkiir = require("linkiir")
function main(Data)
local ok, Msg = pcall(function()
return linkiir.data.extract{ schema = "adt.json", data = Data, type = "hl7" }
end)
if not ok then
-- Record it and move on, rather than stopping the feed.
print("Unparseable message skipped")
return
end
linkiir.flow.push{ data = Msg:text() }
end
Doing it this way makes the decision explicit in the script, where a reviewer can see it, and lets you distinguish the cases you tolerate from the ones you do not. Print a line whenever you swallow something, or a working filter and a broken node look identical to whoever asks why nothing arrived.
For destination-side rejections, the equivalent decision is a node field — Ack Error Handling on Destination LLP. See Destination Nodes.
Retry at the node, not in the script
Transport nodes already handle reconnection. Configure it rather than writing retry loops:
| Node | Fields |
|---|---|
| Destination LLP | Attempt to Reconnect, Reconnect Attempts, Reconnection Interval, Resend on Ack Timeout, Resend Attempts, Ack Timeout |
| Source or Destination File/FTP, with FTP enabled | Attempt to Reconnect, Reconnect Limit Times, Reconnection Interval |
For an outbound call from a script, let the error propagate rather than looping. A retry loop inside main holds the worker and hides the failure from the node's state.
What happens when a node stops
| Guarantee | Detail |
|---|---|
| Messages are not lost | Unprocessed messages wait in the queue |
| Order is preserved | Delivery resumes in order when the node restarts |
| No silent gap | The failure appears in the node state and in log search |
This is why stopping is a safe response to an error. You are pausing a durable queue, not dropping traffic.
Duplicates are possible
Delivery is at-least-once. A message can be delivered twice if processing is interrupted between doing the work and confirming it.
Design the receiver's side for it:
| Destination | Protection |
|---|---|
| Destination LLP | Original ID and message type ACK verification; receiver deduplicates on message control ID |
| Destination File/FTP | Unique ID file naming; FTP Overwrite Handling set to not be uploaded |
| Outbound HTTP | A stable idempotency key derived from the message, if the API supports one |
| Database write | An upsert or guarded insert keyed on a message identifier |
Decide how duplicates are detected before go-live. A replay must not create a second clinical or financial transaction.
Investigating a failure
- Open the node and read its status detail — it carries the last error.
- Search log search for
ERRORlevel records in the project. - Open the failing record and note its correlation ID.
- Search that correlation ID to see how far the message got.
- Copy the archived payload into a sample and reproduce it with Run Test.
Step 5 is the one that saves time. Reproducing with the exact payload beats reasoning about what the sender might have sent.
Replay rather than rewind
To reprocess a message, replay that specific archived payload from log search.
Rewinding a queue consumer to an earlier position reprocesses everything from that point, which usually means re-delivering messages that already succeeded. Replay targets one message.
| Action | Scope | Use for |
|---|---|---|
| Replay an archived message | One message | Normal recovery |
| Bulk replay | A selected set | A known batch that failed together |
| Rewind a consumer position | Everything from that point | Rarely; only with a full understanding of the side effects |
Confirm the receiver tolerates a duplicate before replaying anything.
Safe context to record
Include enough to diagnose without exposing patient data.
Safe: project, workflow, and node names; message ID and correlation ID; error category and code; retry attempt; queue position; timestamps.
Never: raw HL7, FHIR, CDA, or X12 payloads in general service logs; patient name, MRN, date of birth, health card number, address, or phone number in error text, metric labels, or email alerts.
Message payloads are archived deliberately, with access control, in the Log DB. That is where they belong — not in an error string. See Security.