Exceptions
Every exception the bundle can raise, when, and the right Symfony-side response — Voter denials, controller error pages, Messenger retry hints.
The bundle itself does not define exception classes — every
exception raised through OpcuaManager or an injected
OpcUaClientInterface originates in the upstream opcua-client
package. The canonical hierarchy lives in
opcua-client · Exceptions.
This page summarises the slice you most often catch in Symfony controllers, listeners, and Messenger handlers.
Real hierarchy (slice)
All exceptions extend
PhpOpcua\Client\Exception\OpcUaException, which extends
\RuntimeException.
\RuntimeException
└── PhpOpcua\Client\Exception\OpcUaException
├── ConnectionException — TCP / socket level
├── HandshakeException — Hello / OpenSecureChannel rejection
├── SecurityException — policy / mode / trust failures
│ ├── CertificateParseException — malformed cert
│ └── UntrustedCertificateException — cert rejected by trust policy
├── ProtocolException — server returned a non-conforming frame
├── MessageTypeException — wrong OPC UA message tag
├── ServiceException — server returned Bad_*
│ └── ServiceUnsupportedException — Bad_ServiceUnsupported
├── SignatureVerificationException — signature check failed
├── EncodingException — wire codec failure
├── ConfigurationException — bad config at construction time
├── OpenSslException — OpenSSL FFI failure
├── WriteTypeMismatchException — write value vs node type
├── WriteTypeDetectionException — auto-detect could not resolve type
├── CacheCorruptedException — metadata cache corruption
├── InvalidNodeIdException — malformed NodeId string
├── MissingModuleDependencyException — required PHP extension missing
├── ModuleConflictException — conflicting extension version
└── UnsupportedCurveException — ECC curve not supported
The classes commonly cited in older drafts —
PolicyException, CertificateException,
AuthenticationException, InactiveSessionException — do not
exist. The closest real equivalents:
| Older name | Real class |
|---|---|
PolicyException |
SecurityException |
CertificateException |
UntrustedCertificateException (a SecurityException) or CertificateParseException |
AuthenticationException |
ServiceException (server returns Bad_UserAccessDenied / Bad_IdentityTokenRejected) |
InactiveSessionException |
ServiceException (server returns Bad_SessionIdInvalid / Bad_SessionNotActivated) — the client typically reopens on the next call |
The exception classes do not expose
endpoint,statusName,fingerprint,reasonas public properties. The status code is private; use the documented accessors instead (see below).
ConnectionException
TCP layer issues, server unreachable, broken socket.
Causes: server unreachable, firewall, TCP RST mid-flight.
Symfony response:
use PhpOpcua\Client\Exception\ConnectionException;
try {
$dv = $this->client->read('ns=2;s=Speed');
} catch (ConnectionException $e) {
$this->logger->warning('PLC unreachable', ['message' => $e->getMessage()]);
return $this->json(['error' => 'PLC unreachable'], 503);
}
In a Messenger handler:
try {
$dv = $this->client->read('ns=2;s=Speed');
} catch (ConnectionException $e) {
throw new RecoverableMessageHandlingException('Transient', 0, $e);
}
SecurityException
Cert / policy / mode failures during handshake. Subclasses:
UntrustedCertificateException— server cert was rejected by the trust policy.CertificateParseException— server cert was malformed.
Inspect the message for the operator alert; trust pinning is done out-of-band (see Trust store).
HandshakeException
The Hello/OpenSecureChannel exchange was rejected. Typical causes: protocol-version mismatch, security policy not offered, endpoint URL typo.
ServiceException
Server returned a Bad_* status. The connection is healthy;
the operation was rejected.
The status code is private; access it through:
$code = $e->getStatusCode(); // int
$name = \PhpOpcua\Client\Types\StatusCode::getName($code); // string
There is no $statusName public property. Always go through
StatusCode::getName($code).
Common causes:
| Status name | Cause |
|---|---|
Bad_NodeIdInvalid |
Node ID doesn't exist |
Bad_NodeIdUnknown |
Node not in the address space |
Bad_AttributeIdInvalid |
Wrong attribute for the node type |
Bad_TypeMismatch |
Write value doesn't match expected type |
Bad_NotWritable |
Node isn't writable |
Bad_UserAccessDenied |
User lacks permission |
Bad_OutOfRange |
Value outside engineering range |
Bad_SessionIdInvalid |
Session expired server-side |
Response: logic bug or permission misconfig. Surface the status name to the developer/operator.
use PhpOpcua\Client\Exception\ServiceException;
use PhpOpcua\Client\Types\StatusCode;
try {
$this->client->write('ns=2;s=Setpoint', 9999);
} catch (ServiceException $e) {
if (StatusCode::getName($e->getStatusCode()) === 'Bad_OutOfRange') {
return $this->json(['error' => 'value out of range'], 422);
}
throw $e;
}
ServiceUnsupportedException
Specifically Bad_ServiceUnsupported. Subclass of
ServiceException, so existing catch (ServiceException $e)
keeps working.
Cause: server doesn't implement this service set (typical: NodeManagement against UA-.NETStandard).
Response: feature isn't available for this server. Disable the feature in the UI or fallback to a different approach.
EncodingException
Wire codec failure. Typically a server-side bug or protocol-version mismatch.
Response: report upstream. These are rare.
ConfigurationException
Bad config at construction (missing required keys, conflicting options, bad cert path).
Response: fix the config; the exception message points at the field. Symfony's container compilation catches these at boot time.
Symfony exception handler
For unhandled OPC UA exceptions, register a kernel exception listener:
namespace App\EventListener;
use PhpOpcua\Client\Exception\{ConnectionException, SecurityException, ServiceException};
use PhpOpcua\Client\Types\StatusCode;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
final class OpcuaExceptionListener
{
public function __construct(
#[Autowire(service: 'monolog.logger.opcua')]
private LoggerInterface $logger,
) {}
#[AsEventListener(event: ExceptionEvent::class, priority: 0)]
public function __invoke(ExceptionEvent $event): void
{
$e = $event->getThrowable();
if ($e instanceof ConnectionException) {
$this->logger->warning($e->getMessage());
$event->setResponse(new JsonResponse(['error' => 'OPC UA unavailable'], 503));
return;
}
if ($e instanceof SecurityException) {
$this->logger->error('OPC UA security error', ['message' => $e->getMessage()]);
$event->setResponse(new JsonResponse(['error' => 'OPC UA security error'], 502));
return;
}
if ($e instanceof ServiceException) {
$statusName = StatusCode::getName($e->getStatusCode());
$this->logger->error('OPC UA service error', [
'status' => $statusName,
'message' => $e->getMessage(),
]);
$event->setResponse(new JsonResponse([
'error' => 'OPC UA service error',
'status' => $statusName,
'message' => $e->getMessage(),
], 502));
}
}
}
Centralises error responses across all controllers.
Sentry / Bugsnag
Fingerprint by status code (not message):
use PhpOpcua\Client\Exception\ServiceException;
use PhpOpcua\Client\Types\StatusCode;
use Sentry\State\Scope;
\Sentry\configureScope(function (Scope $scope) use ($e) {
if ($e instanceof ServiceException) {
$scope->setFingerprint([
'{{ default }}',
'opcua-service',
StatusCode::getName($e->getStatusCode()),
]);
}
});
Groups Bad_NodeIdInvalid separately from Bad_TypeMismatch.
In tests
use PhpOpcua\Client\Exception\ConnectionException;
use PhpOpcua\Client\Testing\MockClient;
$mock = MockClient::create()
->onRead(function (string $nodeId) {
if ((string) $nodeId === 'ns=2;s=Speed') {
throw new ConnectionException('PLC down');
}
// ...
});
static::getContainer()->set(OpcUaClientInterface::class, $mock);
$client->request('GET', '/api/plc/speed');
$this->assertResponseStatusCodeSame(503);
Where to read next
You've finished Reference. Next: Recipes · Persistent tag history — the canonical end-to-end pipeline.