Error Handling
PLC Error Handling & Exception Management: Building Fault-Tolerant Structured Text
PLCs don't have try/catch — so how do you handle errors? Build fault-tolerant automation with defensive coding patterns, hierarchical fault propagation, and graceful degradation strategies.
The Error Handling Problem in PLCs
High-level languages have try/catch, exceptions, and stack traces. PLCs have... nothing. When something goes wrong in a PLC program — a sensor reads garbage, a division by zero lurks in a calculation, or a communication link drops — the program either produces wrong outputs silently or the CPU faults and everything stops.
Neither outcome is acceptable in industrial automation. A paint line that stops mid-coat ruins a $50,000 car body. A chemical reactor that ignores a failed sensor could cause a runaway reaction. Defensive programming in Structured Text is the difference between a minor alarm and a major incident.
Error Handling Philosophy
Before writing code, establish these principles:
The Universal Error Interface
Every function block you write should include a standardized error interface:
TYPE FB_ErrorStatus :
STRUCT
Error : BOOL; // TRUE = this FB has an active error
ErrorID : DINT; // Unique error code
ErrorMsg : STRING[80]; // Human-readable description
Warning : BOOL; // TRUE = degraded but operational
WarningID : DINT;
FirstErrorTime : STRING[20]; // When the error first occurred
END_STRUCT;
END_TYPE
// Apply to every FB you write
FUNCTION_BLOCK FB_TemperatureControl
VAR_INPUT
Enable : BOOL;
SP_Temp : REAL;
PV_Temp : REAL;
PV_Valid : BOOL; // Sensor health from I/O diagnostics
END_VAR
VAR_OUTPUT
HeatingCmd : REAL; // 0–100%
Status : FB_ErrorStatus;
END_VAR
VAR
pid : FB_PIDController;
lastGoodPV : REAL;
pvFrozenTimer : TON;
prevPV : REAL;
END_VAR// Clear previous errors
Status.Error := FALSE;
Status.Warning := FALSE;
// === INPUT VALIDATION ===
// Check 1: Is the sensor signal valid?
IF NOT PV_Valid THEN
Status.Error := TRUE;
Status.ErrorID := 1001;
Status.ErrorMsg := 'Temperature sensor fault — signal invalid';
HeatingCmd := 0.0; // Fail safe: stop heating
RETURN;
END_IF;
// Check 2: Is the value physically plausible?
IF PV_Temp < -50.0 OR PV_Temp > 500.0 THEN
Status.Error := TRUE;
Status.ErrorID := 1002;
Status.ErrorMsg := 'Temperature out of physical range';
HeatingCmd := 0.0;
RETURN;
END_IF;
// Check 3: Is the sensor frozen (stuck at same value)?
pvFrozenTimer(IN := (ABS(PV_Temp - prevPV) < 0.01), PT := T#60S);
prevPV := PV_Temp;
IF pvFrozenTimer.Q THEN
Status.Warning := TRUE;
Status.WarningID := 2001;
Status.ErrorMsg := 'Temperature sensor may be frozen — no change for 60s';
// Continue operating but flag the issue
END_IF;
// Check 4: Rate-of-change limit (physical impossibility check)
IF ABS(PV_Temp - lastGoodPV) > 50.0 THEN // >50°C in one scan = impossible
Status.Warning := TRUE;
Status.WarningID := 2002;
Status.ErrorMsg := 'Temperature spike detected — using last good value';
PV_Temp := lastGoodPV; // Use last known good value
END_IF;
lastGoodPV := PV_Temp;
// === NORMAL PROCESSING ===
pid(Enable := Enable, SP := SP_Temp, PV := PV_Temp);
HeatingCmd := pid.Output;
// Check 5: Validate output is sane
IF HeatingCmd < 0.0 OR HeatingCmd > 100.0 THEN
HeatingCmd := FC_Clamp(HeatingCmd, 0.0, 100.0);
Status.Warning := TRUE;
Status.WarningID := 2003;
Status.ErrorMsg := 'PID output clamped to safe range';
END_IF;
Division-Safe Math
Division by zero is the most common runtime error in PLC programs. Never trust a denominator:
FUNCTION FC_SafeDivide : REAL
VAR_INPUT
Numerator : REAL;
Denominator : REAL;
DefaultVal : REAL; // Return this if denominator is zero
END_VARIF ABS(Denominator) < 1.0E-9 THEN
FC_SafeDivide := DefaultVal;
ELSE
FC_SafeDivide := Numerator / Denominator;
END_IF;
// Usage patterns
FlowRate := FC_SafeDivide(Volume, TimeDelta, 0.0);
Efficiency := FC_SafeDivide(OutputPower, InputPower, 0.0);
Speed := FC_SafeDivide(Distance, ElapsedTime, 0.0);// Array index safety
FUNCTION FC_SafeArrayIndex : INT
VAR_INPUT
Index : INT;
MinIndex : INT;
MaxIndex : INT;
END_VAR
IF Index < MinIndex THEN
FC_SafeArrayIndex := MinIndex;
ELSIF Index > MaxIndex THEN
FC_SafeArrayIndex := MaxIndex;
ELSE
FC_SafeArrayIndex := Index;
END_IF;
Hierarchical Fault Propagation
Errors must bubble up through the system hierarchy. A sensor fault in a temperature loop should propagate to the process unit, then to the area, then to the plant overview:
FUNCTION_BLOCK FB_ProcessUnit
VAR
tempLoop1 : FB_TemperatureControl;
tempLoop2 : FB_TemperatureControl;
pressLoop : FB_PressureControl;
motorPump : FB_MotorControl; UnitStatus : FB_ErrorStatus;
SubErrors : INT := 0;
SubWarnings : INT := 0;
END_VAR
// Run all sub-FBs
tempLoop1(Enable := TRUE, SP_Temp := 80.0, PV_Temp := AI_Temp1, PV_Valid := AI_Temp1_OK);
tempLoop2(Enable := TRUE, SP_Temp := 65.0, PV_Temp := AI_Temp2, PV_Valid := AI_Temp2_OK);
pressLoop(Enable := TRUE, SP_Press := 3.5, PV_Press := AI_Press1, PV_Valid := AI_Press1_OK);
motorPump(Start := CMD_Start, Stop := CMD_Stop);
// Aggregate errors from children
SubErrors := 0;
SubWarnings := 0;
IF tempLoop1.Status.Error THEN SubErrors := SubErrors + 1; END_IF;
IF tempLoop2.Status.Error THEN SubErrors := SubErrors + 1; END_IF;
IF pressLoop.Status.Error THEN SubErrors := SubErrors + 1; END_IF;
IF motorPump.Status.Error THEN SubErrors := SubErrors + 1; END_IF;
IF tempLoop1.Status.Warning THEN SubWarnings := SubWarnings + 1; END_IF;
IF tempLoop2.Status.Warning THEN SubWarnings := SubWarnings + 1; END_IF;
// Propagate worst-case status
UnitStatus.Error := SubErrors > 0;
UnitStatus.Warning := SubWarnings > 0 AND NOT UnitStatus.Error;
IF SubErrors >= 2 THEN
UnitStatus.ErrorID := 5001;
UnitStatus.ErrorMsg := 'Multiple sub-system faults — unit shutdown';
// Trigger safe shutdown sequence
ELSIF SubErrors = 1 THEN
UnitStatus.ErrorID := 5002;
UnitStatus.ErrorMsg := 'Single sub-system fault — degraded operation';
// Continue with degraded capability
END_IF;
Watchdog Pattern
A watchdog verifies that a process is still alive and progressing. If a step takes too long, something is wrong:
FUNCTION_BLOCK FB_Watchdog
VAR_INPUT
Enable : BOOL;
MonitoredBit : BOOL; // Should change/pulse periodically
MaxSilenceTime : TIME; // How long before alarm
END_VAR
VAR_OUTPUT
Healthy : BOOL;
TimedOut : BOOL;
SilenceSecs : REAL;
END_VAR
VAR
tmrSilence : TON;
lastState : BOOL;
stateChanged : BOOL;
END_VARIF NOT Enable THEN
Healthy := FALSE;
TimedOut := FALSE;
RETURN;
END_IF;
// Detect any change in the monitored bit
stateChanged := (MonitoredBit <> lastState);
lastState := MonitoredBit;
// Reset timer on any change
IF stateChanged THEN
tmrSilence(IN := FALSE);
END_IF;
tmrSilence(IN := NOT stateChanged, PT := MaxSilenceTime);
TimedOut := tmrSilence.Q;
Healthy := NOT TimedOut;
// Usage: monitor a communication heartbeat
VAR
wdgSCADA : FB_Watchdog;
wdgPLC2 : FB_Watchdog;
END_VARwdgSCADA(Enable := TRUE, MonitoredBit := SCADA_Heartbeat, MaxSilenceTime := T#10S);
wdgPLC2(Enable := TRUE, MonitoredBit := PLC2_Heartbeat, MaxSilenceTime := T#5S);
IF wdgSCADA.TimedOut THEN
// SCADA link lost — switch to local control
END_IF;
IF wdgPLC2.TimedOut THEN
// Peer PLC not responding — safe state
END_IF;
Graceful Degradation Strategies
When a component fails, don't stop everything. Degrade gracefully:
| Failure | Bad Response | Good Response | | Temperature sensor fails | Shut down process | Use last known good value + alarm + reduced rate | | Communication to VFD lost | Stop conveyor | Hold last speed command + alarm + timeout to safe stop | | One of two redundant pumps faults | Stop flow | Run single pump at higher speed + maintenance alarm | | HMI link lost | Operators blind | PLC continues on last setpoints + auto-mode | | Recipe download fails | Halt batch | Re-request + retry + hold at current step |
FUNCTION_BLOCK FB_RedundantSensor
VAR_INPUT
PV_Primary : REAL;
PV_Backup : REAL;
Primary_Valid : BOOL;
Backup_Valid : BOOL;
DeviationMax : REAL := 5.0;
END_VAR
VAR_OUTPUT
PV_Output : REAL;
ActiveSensor : INT; // 1=Primary, 2=Backup, 0=None
SensorMismatch: BOOL;
Status : FB_ErrorStatus;
END_VARIF Primary_Valid AND Backup_Valid THEN
// Both healthy — check agreement
IF ABS(PV_Primary - PV_Backup) > DeviationMax THEN
SensorMismatch := TRUE;
Status.Warning := TRUE;
Status.WarningID := 3001;
Status.ErrorMsg := 'Sensor mismatch — deviation exceeds limit';
END_IF;
PV_Output := PV_Primary; // Primary is preferred
ActiveSensor := 1;
ELSIF Primary_Valid THEN
PV_Output := PV_Primary;
ActiveSensor := 1;
Status.Warning := TRUE;
Status.WarningID := 3002;
Status.ErrorMsg := 'Backup sensor offline — no redundancy';
ELSIF Backup_Valid THEN
PV_Output := PV_Backup;
ActiveSensor := 2;
Status.Warning := TRUE;
Status.WarningID := 3003;
Status.ErrorMsg := 'Primary sensor failed — running on backup';
ELSE
ActiveSensor := 0;
Status.Error := TRUE;
Status.ErrorID := 3004;
Status.ErrorMsg := 'Both sensors failed — no valid measurement';
END_IF;
Error Code Numbering System
Organize error codes so technicians can quickly identify the source:
| Range | Source | Example | | 1000–1999 | Analog inputs / sensors | 1001 = Sensor signal invalid | | 2000–2999 | Control loop warnings | 2001 = Sensor frozen | | 3000–3999 | Redundancy / diagnostics | 3003 = Primary sensor failed | | 4000–4999 | Communication | 4001 = Modbus timeout | | 5000–5999 | Process unit level | 5001 = Multiple sub-faults | | 6000–6999 | Motor / actuator | 6001 = VFD fault | | 7000–7999 | Sequence / batch | 7001 = Step timeout | | 8000–8999 | Safety system | 8001 = E-stop activated | | 9000–9999 | System / infrastructure | 9001 = CPU overload |
// Error code lookup — useful for HMI display
FUNCTION FC_ErrorCodeToText : STRING[80]
VAR_INPUT
ErrorCode : DINT;
END_VARCASE ErrorCode OF
1001: FC_ErrorCodeToText := 'Sensor signal invalid — check wiring';
1002: FC_ErrorCodeToText := 'Value out of physical range';
2001: FC_ErrorCodeToText := 'Sensor frozen — no change detected';
2002: FC_ErrorCodeToText := 'Spike detected — rate-of-change exceeded';
3001: FC_ErrorCodeToText := 'Sensor mismatch — check calibration';
4001: FC_ErrorCodeToText := 'Communication timeout — check network';
5001: FC_ErrorCodeToText := 'Multiple faults — unit shutdown initiated';
6001: FC_ErrorCodeToText := 'Drive fault — check VFD display';
7001: FC_ErrorCodeToText := 'Sequence step timeout — check process';
8001: FC_ErrorCodeToText := 'Emergency stop activated';
ELSE
FC_ErrorCodeToText := 'Unknown error code';
END_CASE;
Defensive Programming Checklist
Apply these checks to every function block you write:
Summary
| Pattern | Purpose | | Standard error interface | Every FB reports Error, ErrorID, ErrorMsg, Warning | | Input validation | Range, plausibility, frozen-sensor, rate-of-change checks | | Safe math | FC_SafeDivide, FC_SafeArrayIndex — never trust denominators | | Fault propagation | Errors bubble up through FB hierarchy to plant overview | | Watchdog | Detect stalled processes and lost communication | | Graceful degradation | Redundant sensors, last-known-good values, reduced-rate operation | | Error code system | Numbered ranges by source for fast troubleshooting |
The goal isn't to prevent all errors — that's impossible. The goal is to detect them immediately, respond safely, and give the operator the information they need to fix it. Every unhandled error in your PLC code is a latent incident waiting for the right conditions.