Repository: gopcua/opcua
Pull request #676 (2023-08-21 11:56:46)

Readme: adjust  Services section
as noted in #675 this PR adjusts the README to clarify usage of services


Patch:
diff --git a/.gitignore b/.gitignore
index 971ffc6..6a9856f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,3 @@ build/
 dist/
 __pycache__/
 .vscode/
-.idea/
diff --git a/README.md b/README.md
index 680136c..eab1226 100644
--- a/README.md
+++ b/README.md
@@ -168,49 +168,49 @@ The current focus is on the OPC UA Binary protocol over TCP. No other protocols
 
 ### Services
 
-The current set of supported services is only for the high-level client.
-
-| Service Set                 | Service                       | Supported | Notes        |
-|-----------------------------|-------------------------------|-----------|--------------|
-| Discovery Service Set       | FindServers                   |           |              |
-|                             | FindServersOnNetwork          |           |              |
-|                             | GetEndpoints                  | Yes       |              |
-|                             | RegisterServer                |           |              |
-|                             | RegisterServer2               |           |              |
-| Secure Channel Service Set  | OpenSecureChannel             | Yes       |              |
-|                             | CloseSecureChannel            | Yes       |              |
-| Session Service Set         | CreateSession                 | Yes       |              |
-|                             | CloseSession                  | Yes       |              |
-|                             | ActivateSession               | Yes       |              |
-|                             | Cancel                        |           |              |
-| Node Management Service Set | AddNodes                      |           |              |
-|                             | AddReferences                 |           |              |
-|                             | DeleteNodes                   |           |              |
-|                             | DeleteReferences              |           |              |
-| View Service Set            | Browse                        | Yes       |              |
-|                             | BrowseNext                    | Yes       |              |
-|                             | TranslateBrowsePathsToNodeIds |           |              |
-|                             | RegisterNodes                 | Yes       |              |
-|                             | UnregisterNodes               | Yes       |              |
-| Query Service Set           | QueryFirst                    |           |              |
-|                             | QueryNext                     |           |              |
-| Attribute Service Set       | Read                          | Yes       |              |
-|                             | Write                         | Yes       |              |
-|                             | HistoryRead                   | Yes       |              |
-|                             | HistoryUpdate                 |           |              |
-| Method Service Set          | Call                          | Yes       |              |
-| MonitoredItems Service Set  | CreateMonitoredItems          | Yes       |              |
-|                             | DeleteMonitoredItems          | Yes       |              |
-|                             | ModifyMonitoredItems          | Yes       |              |
-|                             | SetMonitoringMode             |           |              |
-|                             | SetTriggering                 |           |              |
-| Subscription Service Set    | CreateSubscription            | Yes       |              |
-|                             | ModifySubscription            |           |              |
-|                             | SetPublishingMode             |           |              |
-|                             | Publish                       | Yes       |              |
-|                             | Republish                     |           |              |
-|                             | DeleteSubscriptions           | Yes       |              |
-|                             | TransferSubscriptions         |           |              |
+Here is the current set of supported services. For low-level access use the client `Send` function directly.
+
+| Service Set                 | Service                       | Client | Notes        |
+|-----------------------------|-------------------------------|--------|--------------|
+| Discovery Service Set       | FindServers                   |        |              |
+|                             | FindServersOnNetwork          |        |              |
+|                             | GetEndpoints                  | Yes    |              |
+|                             | RegisterServer                |        |              |
+|                             | RegisterServer2               |        |              |
+| Secure Channel Service Set  | OpenSecureChannel             | Yes    |              |
+|                             | CloseSecureChannel            | Yes    |              |
+| Session Service Set         | CreateSession                 | Yes    |              |
+|                             | CloseSession                  | Yes    |              |
+|                             | ActivateSession               | Yes    |              |
+|                             | Cancel                        |        |              |
+| Node Management Service Set | AddNodes                      |        |              |
+|                             | AddReferences                 |        |              |
+|                             | DeleteNodes                   |        |              |
+|                             | DeleteReferences              |        |              |
+| View Service Set            | Browse                        | Yes    |              |
+|                             | BrowseNext                    | Yes    |              |
+|                             | TranslateBrowsePathsToNodeIds |        |              |
+|                             | RegisterNodes                 | Yes    |              |
+|                             | UnregisterNodes               | Yes    |              |
+| Query Service Set           | QueryFirst                    |        |              |
+|                             | QueryNext                     |        |              |
+| Attribute Service Set       | Read                          | Yes    |              |
+|                             | Write                         | Yes    |              |
+|                             | HistoryRead                   | Yes    |              |
+|                             | HistoryUpdate                 |        |              |
+| Method Service Set          | Call                          | Yes    |              |
+| MonitoredItems Service Set  | CreateMonitoredItems          | Yes    |              |
+|                             | DeleteMonitoredItems          | Yes    |              |
+|                             | ModifyMonitoredItems          | Yes    |              |
+|                             | SetMonitoringMode             |        |              |
+|                             | SetTriggering                 |        |              |
+| Subscription Service Set    | CreateSubscription            | Yes    |              |
+|                             | ModifySubscription            |        |              |
+|                             | SetPublishingMode             |        |              |
+|                             | Publish                       | Yes    |              |
+|                             | Republish                     |        |              |
+|                             | DeleteSubscriptions           | Yes    |              |
+|                             | TransferSubscriptions         |        |              |
 
 ## Authors
 
diff --git a/client.go b/client.go
index 67bbb0a..17cadd2 100644
--- a/client.go
+++ b/client.go
@@ -30,10 +30,7 @@ import (
 // GetEndpoints returns the available endpoint descriptions for the server.
 func GetEndpoints(ctx context.Context, endpoint string, opts ...Option) ([]*ua.EndpointDescription, error) {
 	opts = append(opts, AutoReconnect(false))
-	c, err := NewClient(endpoint, opts...)
-	if err != nil {
-		return nil, err
-	}
+	c := NewClient(endpoint, opts...)
 	if err := c.Dial(ctx); err != nil {
 		return nil, err
 	}
@@ -136,6 +133,15 @@ type Client struct {
 
 	// monitorOnce ensures only one connection monitor is running
 	monitorOnce sync.Once
+
+	// cfgerr contains an error that was captured in ApplyConfig.
+	// Since the API does not allow to bubble the error up in NewClient
+	// and we don't want to break existing code right away we carry the
+	// error here and bubble it up during Dial and Connect.
+	//
+	// Note: Starting with v0.5 NewClient will return the error and this
+	// variable needs to be removed.
+	cfgerr error
 }
 
 // NewClient creates a new Client.
@@ -149,11 +155,10 @@ type Client struct {
 // #Option for details.
 //
 // https://godoc.org/github.com/gopcua/opcua#Option
-func NewClient(endpoint string, opts ...Option) (*Client, error) {
-	cfg, err := ApplyConfig(opts...)
-	if err != nil {
-		return nil, err
-	}
+//
+// Note: Starting with v0.5 this function will return an error.
+func NewClient(endpoint string, opts ...Option) *Client {
+	cfg := ApplyConfig(opts...)
 	c := Client{
 		endpointURL: endpoint,
 		cfg:         cfg,
@@ -162,6 +167,7 @@ func NewClient(endpoint string, opts ...Option) (*Client, error) {
 		pendingAcks: make([]*ua.SubscriptionAcknowledgement, 0),
 		pausech:     make(chan struct{}, 2),
 		resumech:    make(chan struct{}, 2),
+		cfgerr:      cfg.Error(), // todo(fs): remove with v0.5.0 and return the error
 	}
 	c.pauseSubscriptions(context.Background())
 	c.setPublishTimeout(uasc.MaxTimeout)
@@ -169,7 +175,7 @@ func NewClient(endpoint string, opts ...Option) (*Client, error) {
 	c.setSecureChannel(nil)
 	c.setSession(nil)
 	c.setNamespaces([]string{})
-	return &c, nil
+	return &c
 }
 
 // reconnectAction is a list of actions for the client reconnection logic.
@@ -188,6 +194,11 @@ const (
 
 // Connect establishes a secure channel and creates a new session.
 func (c *Client) Connect(ctx context.Context) error {
+	// todo(fs): remove with v0.5.0
+	if c.cfgerr != nil {
+		return c.cfgerr
+	}
+
 	// todo(fs): the secure channel is 'nil' during a re-connect
 	// todo(fs): but we expect this method to be called once during startup
 	// todo(fs): so this is probably safe
@@ -492,16 +503,16 @@ func (c *Client) monitor(ctx context.Context) {
 						// populated in the previous step.
 
 						activeSubs = 0
-						for _, subID := range subsToRepublish {
-							if err := c.republishSubscription(ctx, subID, availableSeqs[subID]); err != nil {
-								dlog.Printf("republish of subscription %d failed", subID)
-								subsToRecreate = append(subsToRecreate, subID)
+						for _, id := range subsToRepublish {
+							if err := c.republishSubscription(ctx, id, availableSeqs[id]); err != nil {
+								dlog.Printf("republish of subscription %d failed", id)
+								subsToRecreate = append(subsToRecreate, id)
 							}
 							activeSubs++
 						}
 
-						for _, subID := range subsToRecreate {
-							if err := c.recreateSubscription(ctx, subID); err != nil {
+						for _, id := range subsToRecreate {
+							if err := c.recreateSubscription(ctx, id); err != nil {
 								dlog.Printf("recreate subscripitions failed: %v", err)
 								action = recreateSession
 								continue
@@ -544,6 +555,11 @@ func (c *Client) monitor(ctx context.Context) {
 
 // Dial establishes a secure channel.
 func (c *Client) Dial(ctx context.Context) error {
+	// todo(fs): remove with v0.5.0
+	if c.cfgerr != nil {
+		return c.cfgerr
+	}
+
 	stats.Client().Add("Dial", 1)
 
 	if c.SecureChannel() != nil {
@@ -738,16 +754,10 @@ func (c *Client) CreateSession(ctx context.Context, cfg *uasc.SessionConfig) (*S
 		// Ensure we have a valid identity token that the server will accept before trying to activate a session
 		if c.cfg.session.UserIdentityToken == nil {
 			opt := AuthAnonymous()
-			// todo(sr): opt returns an error but we concluded that this call cannot
-			// todo(sr): fail and that we do not want to stop creating the session
-			// todo(sr): hence we ignore it.
 			opt(c.cfg)
 
 			p := anonymousPolicyID(res.ServerEndpoints)
 			opt = AuthPolicyID(p)
-			// todo(sr): opt returns an error but we concluded that this call cannot
-			// todo(sr): fail and that we do not want to stop creating the session
-			// todo(sr): hence we ignore it.
 			opt(c.cfg)
 		}
 
diff --git a/config.go b/config.go
index ff42433..fd92a31 100644
--- a/config.go
+++ b/config.go
@@ -56,6 +56,18 @@ type Config struct {
 	dialer  *uacp.Dialer
 	sechan  *uasc.Config
 	session *uasc.SessionConfig
+	err     error
+}
+
+func (cfg *Config) setError(err error) {
+	if cfg.err != nil {
+		return
+	}
+	cfg.err = err
+}
+
+func (cfg *Config) Error() error {
+	return cfg.err
 }
 
 // NewDialer creates a uacp.Dialer from the config options
@@ -68,76 +80,68 @@ func NewDialer(cfg *Config) *uacp.Dialer {
 
 // ApplyConfig applies the config options to the default configuration.
 // todo(fs): Can we find a better name?
-func ApplyConfig(opts ...Option) (*Config, error) {
+//
+// Note: Starting with v0.5 this function will return an error.
+func ApplyConfig(opts ...Option) *Config {
 	cfg := &Config{
 		sechan:  DefaultClientConfig(),
 		session: DefaultSessionConfig(),
 	}
-	var errs []error
 	for _, opt := range opts {
-		if err := opt(cfg); err != nil {
-			errs = append(errs, err)
-		}
+		opt(cfg)
 	}
-	return cfg, errors.Join(errs...)
+	return cfg
 }
 
 // Option is an option function type to modify the configuration.
-type Option func(*Config) error
+type Option func(*Config)
 
 // ApplicationName sets the application name in the session configuration.
 func ApplicationName(s string) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.session.ClientDescription.ApplicationName = ua.NewLocalizedText(s)
-		return nil
 	}
 }
 
 // ApplicationURI sets the application uri in the session configuration.
 func ApplicationURI(s string) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.session.ClientDescription.ApplicationURI = s
-		return nil
 	}
 }
 
 // AutoReconnect sets the auto reconnect state of the secure channel.
 func AutoReconnect(b bool) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.sechan.AutoReconnect = b
-		return nil
 	}
 }
 
 // ReconnectInterval is interval duration between each reconnection attempt.
 func ReconnectInterval(d time.Duration) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.sechan.ReconnectInterval = d
-		return nil
 	}
 }
 
 // Lifetime sets the lifetime of the secure channel in milliseconds.
 func Lifetime(d time.Duration) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.sechan.Lifetime = uint32(d / time.Millisecond)
-		return nil
 	}
 }
 
 // Locales sets the locales in the session configuration.
 func Locales(locale ...string) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.session.LocaleIDs = locale
-		return nil
 	}
 }
 
 // ProductURI sets the product uri in the session configuration.
 func ProductURI(s string) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.session.ClientDescription.ProductURI = s
-		return nil
 	}
 }
 
@@ -150,103 +154,95 @@ var randomRequestID func() uint32 = nil
 // is the caller's responsibility to initialize the random number
 // generator properly.
 func RandomRequestID() Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		if randomRequestID != nil {
 			cfg.sechan.RequestIDSeed = randomRequestID()
 		} else {
 			cfg.sechan.RequestIDSeed = uint32(rand.Int31())
 		}
-		return nil
 	}
 }
 
 // RemoteCertificate sets the server certificate.
 func RemoteCertificate(cert []byte) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.sechan.RemoteCertificate = cert
-		return nil
 	}
 }
 
 // RemoteCertificateFile sets the server certificate from the file
 // in PEM or DER encoding.
 func RemoteCertificateFile(filename string) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		if filename == "" {
-			return nil
+			return
 		}
 
 		cert, err := loadCertificate(filename)
 		if err != nil {
-			return err
+			cfg.setError(err)
+			return
 		}
 		cfg.sechan.RemoteCertificate = cert
-		return nil
 	}
 }
 
 // SecurityMode sets the security mode for the secure channel.
 func SecurityMode(m ua.MessageSecurityMode) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.sechan.SecurityMode = m
-		return nil
 	}
 }
 
 // SecurityModeString sets the security mode for the secure channel.
 // Valid values are "None", "Sign", and "SignAndEncrypt".
 func SecurityModeString(s string) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.sechan.SecurityMode = ua.MessageSecurityModeFromString(s)
-		return nil
 	}
 }
 
 // SecurityPolicy sets the security policy uri for the secure channel.
 func SecurityPolicy(s string) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.sechan.SecurityPolicyURI = ua.FormatSecurityPolicyURI(s)
-		return nil
 	}
 }
 
 // SessionName sets the name in the session configuration.
 func SessionName(s string) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.session.SessionName = s
-		return nil
 	}
 }
 
 // SessionTimeout sets the timeout in the session configuration.
 func SessionTimeout(d time.Duration) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.session.SessionTimeout = d
-		return nil
 	}
 }
 
 // PrivateKey sets the RSA private key in the secure channel configuration.
 func PrivateKey(key *rsa.PrivateKey) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.sechan.LocalKey = key
-		return nil
 	}
 }
 
 // PrivateKeyFile sets the RSA private key in the secure channel configuration
 // from a PEM or DER encoded file.
 func PrivateKeyFile(filename string) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		if filename == "" {
-			return nil
+			return
 		}
 		key, err := loadPrivateKey(filename)
 		if err != nil {
-			return err
+			cfg.setError(err)
+			return
 		}
 		cfg.sechan.LocalKey = key
-		return nil
 	}
 }
 
@@ -275,27 +271,26 @@ func loadPrivateKey(filename string) (*rsa.PrivateKey, error) {
 // Certificate sets the client X509 certificate in the secure channel configuration.
 // It also detects and sets the ApplicationURI from the URI within the certificate.
 func Certificate(cert []byte) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		setCertificate(cert, cfg)
-		return nil
 	}
 }
 
-// CertificateFile sets the client X509 certificate in the secure channel configuration
+// Certificate sets the client X509 certificate in the secure channel configuration
 // from the PEM or DER encoded file. It also detects and sets the ApplicationURI
 // from the URI within the certificate.
 func CertificateFile(filename string) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		if filename == "" {
-			return nil
+			return
 		}
 
 		cert, err := loadCertificate(filename)
 		if err != nil {
-			return err
+			cfg.setError(err)
+			return
 		}
 		setCertificate(cert, cfg)
-		return nil
 	}
 }
 
@@ -338,7 +333,7 @@ func setCertificate(cert []byte, cfg *Config) {
 // SecurityFromEndpoint sets the server-related security parameters from
 // a chosen endpoint (received from GetEndpoints())
 func SecurityFromEndpoint(ep *ua.EndpointDescription, authType ua.UserTokenType) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.sechan.SecurityPolicyURI = ep.SecurityPolicyURI
 		cfg.sechan.SecurityMode = ep.SecurityMode
 		cfg.sechan.RemoteCertificate = ep.ServerCertificate
@@ -364,14 +359,13 @@ func SecurityFromEndpoint(ep *ua.EndpointDescription, authType ua.UserTokenType)
 
 			setPolicyID(cfg.session.UserIdentityToken, t.PolicyID)
 			cfg.session.AuthPolicyURI = t.SecurityPolicyURI
-			return nil
+			return
 		}
 
 		if cfg.session.UserIdentityToken == nil {
 			cfg.session.UserIdentityToken = &ua.AnonymousIdentityToken{PolicyID: defaultAnonymousPolicyID}
 			cfg.session.AuthPolicyURI = ua.SecurityPolicyURINone
 		}
-		return nil
 	}
 }
 
@@ -395,13 +389,12 @@ func setPolicyID(t interface{}, policy string) {
 // todo(fs): AuthXXX methods since this approach requires context
 // todo(fs): and ordering?
 func AuthPolicyID(policy string) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		if cfg.session.UserIdentityToken == nil {
 			log.Printf("policy ID needs to be set after the policy type is chosen, no changes made.  Call SecurityFromEndpoint() or an AuthXXX() option first")
-			return nil
+			return
 		}
 		setPolicyID(cfg.session.UserIdentityToken, policy)
-		return nil
 	}
 }
 
@@ -409,7 +402,7 @@ func AuthPolicyID(policy string) Option {
 // Note: PolicyID still needs to be set outside of this method, typically through
 // the SecurityFromEndpoint() Option
 func AuthAnonymous() Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		if cfg.session.UserIdentityToken == nil {
 			cfg.session.UserIdentityToken = &ua.AnonymousIdentityToken{}
 		}
@@ -418,9 +411,8 @@ func AuthAnonymous() Option {
 		if !ok {
 			// todo(fs): should we Fatal here?
 			log.Printf("non-anonymous authentication already configured, ignoring")
-			return nil
+			return
 		}
-		return nil
 	}
 }
 
@@ -428,7 +420,7 @@ func AuthAnonymous() Option {
 // Note: PolicyID still needs to be set outside of this method, typically through
 // the SecurityFromEndpoint() Option
 func AuthUsername(user, pass string) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		if cfg.session.UserIdentityToken == nil {
 			cfg.session.UserIdentityToken = &ua.UserNameIdentityToken{}
 		}
@@ -437,12 +429,11 @@ func AuthUsername(user, pass string) Option {
 		if !ok {
 			// todo(fs): should we Fatal here?
 			log.Printf("non-username authentication already configured, ignoring")
-			return nil
+			return
 		}
 
 		t.UserName = user
 		cfg.session.AuthPassword = pass
-		return nil
 	}
 }
 
@@ -450,7 +441,7 @@ func AuthUsername(user, pass string) Option {
 // Note: PolicyID still needs to be set outside of this method, typically through
 // the SecurityFromEndpoint() Option
 func AuthCertificate(cert []byte) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		if cfg.session.UserIdentityToken == nil {
 			cfg.session.UserIdentityToken = &ua.X509IdentityToken{}
 		}
@@ -459,11 +450,10 @@ func AuthCertificate(cert []byte) Option {
 		if !ok {
 			// todo(fs): should we Fatal here?
 			log.Printf("non-certificate authentication already configured, ignoring")
-			return nil
+			return
 		}
 
 		t.CertificateData = cert
-		return nil
 	}
 }
 
@@ -471,7 +461,7 @@ func AuthCertificate(cert []byte) Option {
 // Note: PolicyID still needs to be set outside of this method, typically through
 // the SecurityFromEndpoint() Option
 func AuthIssuedToken(tokenData []byte) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		if cfg.session.UserIdentityToken == nil {
 			cfg.session.UserIdentityToken = &ua.IssuedIdentityToken{}
 		}
@@ -479,75 +469,67 @@ func AuthIssuedToken(tokenData []byte) Option {
 		t, ok := cfg.session.UserIdentityToken.(*ua.IssuedIdentityToken)
 		if !ok {
 			log.Printf("non-issued token authentication already configured, ignoring")
-			return nil
+			return
 		}
 
 		// todo(dw): not correct; need to read spec
 		t.TokenData = tokenData
-		return nil
 	}
 }
 
 // RequestTimeout sets the timeout for all requests over SecureChannel
 func RequestTimeout(t time.Duration) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		cfg.sechan.RequestTimeout = t
-		return nil
 	}
 }
 
 // Dialer sets the uacp.Dialer to establish the connection to the server.
 func Dialer(d *uacp.Dialer) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		initDialer(cfg)
 		cfg.dialer = d
-		return nil
 	}
 }
 
 // DialTimeout sets the timeout for establishing the UACP connection.
 // Defaults to DefaultDialTimeout. Set to zero for no timeout.
 func DialTimeout(d time.Duration) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		initDialer(cfg)
 		cfg.dialer.Dialer.Timeout = d
-		return nil
 	}
 }
 
 // MaxMessageSize sets the maximum message size for the UACP handshake.
 func MaxMessageSize(n uint32) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		initDialer(cfg)
 		cfg.dialer.ClientACK.MaxMessageSize = n
-		return nil
 	}
 }
 
 // MaxChunkCount sets the maximum chunk count for the UACP handshake.
 func MaxChunkCount(n uint32) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		initDialer(cfg)
 		cfg.dialer.ClientACK.MaxChunkCount = n
-		return nil
 	}
 }
 
 // ReceiveBufferSize sets the receive buffer size for the UACP handshake.
 func ReceiveBufferSize(n uint32) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		initDialer(cfg)
 		cfg.dialer.ClientACK.ReceiveBufSize = n
-		return nil
 	}
 }
 
 // SendBufferSize sets the send buffer size for the UACP handshake.
 func SendBufferSize(n uint32) Option {
-	return func(cfg *Config) error {
+	return func(cfg *Config) {
 		initDialer(cfg)
 		cfg.dialer.ClientACK.SendBufSize = n
-		return nil
 	}
 }
 
diff --git a/errors/errors.go b/errors/errors.go
index 7280f35..bfc84da 100644
--- a/errors/errors.go
+++ b/errors/errors.go
@@ -34,11 +34,6 @@ func Unwrap(err error) error {
 	return errors.Unwrap(err)
 }
 
-// Join wraps errors.Join
-func Join(errs ...error) error {
-	return errors.Join(errs...)
-}
-
 // Equal returns true if the two errors have the same error message.
 //
 // todo(fs): the reason we need this function and cannot just use
diff --git a/examples/accesslevel/accesslevel.go b/examples/accesslevel/accesslevel.go
index a6ee453..39bc234 100644
--- a/examples/accesslevel/accesslevel.go
+++ b/examples/accesslevel/accesslevel.go
@@ -25,10 +25,7 @@ func main() {
 
 	ctx := context.Background()
 
-	c, err := opcua.NewClient(*endpoint)
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(*endpoint)
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/browse/browse.go b/examples/browse/browse.go
index 27826b2..2287662 100644
--- a/examples/browse/browse.go
+++ b/examples/browse/browse.go
@@ -174,10 +174,7 @@ func main() {
 
 	ctx := context.Background()
 
-	c, err := opcua.NewClient(*endpoint)
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(*endpoint)
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/crypto/crypto.go b/examples/crypto/crypto.go
index 76b92d9..dc7b620 100644
--- a/examples/crypto/crypto.go
+++ b/examples/crypto/crypto.go
@@ -63,10 +63,7 @@ func main() {
 	opts := clientOptsFromFlags(endpoints)
 
 	// Create a Client with the selected options
-	c, err := opcua.NewClient(*endpoint, opts...)
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(*endpoint, opts...)
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
@@ -89,10 +86,7 @@ func main() {
 		log.Fatalf("Error detaching session: %s", err)
 	}
 
-	d, err := opcua.NewClient(*endpoint, opts...)
-	if err != nil {
-		log.Fatal(err)
-	}
+	d := opcua.NewClient(*endpoint, opts...)
 
 	// Create a channel only and do not activate it automatically
 	d.Dial(ctx)
diff --git a/examples/datetime/datetime.go b/examples/datetime/datetime.go
index dca8063..ad2ae77 100644
--- a/examples/datetime/datetime.go
+++ b/examples/datetime/datetime.go
@@ -47,10 +47,7 @@ func main() {
 		opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous),
 	}
 
-	c, err := opcua.NewClient(ep.EndpointURL, opts...)
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(ep.EndpointURL, opts...)
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/history-read/history-read.go b/examples/history-read/history-read.go
index c0ca3c0..d3cc2b5 100644
--- a/examples/history-read/history-read.go
+++ b/examples/history-read/history-read.go
@@ -24,10 +24,7 @@ func main() {
 
 	ctx := context.Background()
 
-	c, err := opcua.NewClient(*endpoint)
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(*endpoint)
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/method/method.go b/examples/method/method.go
index 7a16585..802dcd7 100644
--- a/examples/method/method.go
+++ b/examples/method/method.go
@@ -24,10 +24,7 @@ func main() {
 
 	ctx := context.Background()
 
-	c, err := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/monitor/monitor.go b/examples/monitor/monitor.go
index ef0f39c..8700cbe 100644
--- a/examples/monitor/monitor.go
+++ b/examples/monitor/monitor.go
@@ -63,10 +63,7 @@ func main() {
 		opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous),
 	}
 
-	c, err := opcua.NewClient(ep.EndpointURL, opts...)
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(ep.EndpointURL, opts...)
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/read/read.go b/examples/read/read.go
index e33763f..a921e02 100644
--- a/examples/read/read.go
+++ b/examples/read/read.go
@@ -28,10 +28,7 @@ func main() {
 
 	ctx := context.Background()
 
-	c, err := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/regread/regread.go b/examples/regread/regread.go
index 01eb013..ca2ca8b 100644
--- a/examples/regread/regread.go
+++ b/examples/regread/regread.go
@@ -25,10 +25,7 @@ func main() {
 
 	ctx := context.Background()
 
-	c, err := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/subscribe/subscribe.go b/examples/subscribe/subscribe.go
index 8127cd9..32134d2 100644
--- a/examples/subscribe/subscribe.go
+++ b/examples/subscribe/subscribe.go
@@ -60,10 +60,7 @@ func main() {
 		opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous),
 	}
 
-	c, err := opcua.NewClient(ep.EndpointURL, opts...)
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(ep.EndpointURL, opts...)
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/translate/translate.go b/examples/translate/translate.go
index 48415a0..3f9c6ff 100644
--- a/examples/translate/translate.go
+++ b/examples/translate/translate.go
@@ -27,10 +27,7 @@ func main() {
 
 	ctx := context.Background()
 
-	c, err := opcua.NewClient(*endpoint)
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(*endpoint)
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/trigger/trigger.go b/examples/trigger/trigger.go
index beafb3b..e6f685f 100644
--- a/examples/trigger/trigger.go
+++ b/examples/trigger/trigger.go
@@ -59,10 +59,7 @@ func main() {
 		opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous),
 	}
 
-	c, err := opcua.NewClient(ep.EndpointURL, opts...)
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(ep.EndpointURL, opts...)
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/udt/udt.go b/examples/udt/udt.go
index 6fa3ad5..b7eebdd 100644
--- a/examples/udt/udt.go
+++ b/examples/udt/udt.go
@@ -55,10 +55,7 @@ func main() {
 		opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous),
 	}
 
-	c, err := opcua.NewClient(ep.EndpointURL, opts...)
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(ep.EndpointURL, opts...)
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/write/write.go b/examples/write/write.go
index ca97d24..62f9dd9 100644
--- a/examples/write/write.go
+++ b/examples/write/write.go
@@ -26,10 +26,7 @@ func main() {
 
 	ctx := context.Background()
 
-	c, err := opcua.NewClient(*endpoint)
-	if err != nil {
-		log.Fatal(err)
-	}
+	c := opcua.NewClient(*endpoint)
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}


Test patch:
diff --git a/client_test.go b/client_test.go
index b64ad50..a1b12d8 100644
--- a/client_test.go
+++ b/client_test.go
@@ -11,11 +11,8 @@ import (
 )
 
 func TestClient_Send_DoesNotPanicWhenDisconnected(t *testing.T) {
-	c, err := NewClient("opc.tcp://example.com:4840")
-	if err != nil {
-		t.Fatal(err)
-	}
-	err = c.Send(context.Background(), &ua.ReadRequest{}, func(i interface{}) error {
+	c := NewClient("opc.tcp://example.com:4840")
+	err := c.Send(context.Background(), &ua.ReadRequest{}, func(i interface{}) error {
 		return nil
 	})
 	verify.Values(t, "", err, ua.StatusBadServerNotConnected)
diff --git a/config_test.go b/config_test.go
index 602db96..42d7204 100644
--- a/config_test.go
+++ b/config_test.go
@@ -170,7 +170,6 @@ func TestOptions(t *testing.T) {
 		name string
 		opt  Option
 		cfg  *Config
-		err  error
 	}{
 		{
 			name: `ApplicationName("a")`,
@@ -298,8 +297,9 @@ func TestOptions(t *testing.T) {
 		{
 			name: `CertificateFile() error`,
 			opt:  CertificateFile("x"),
-			cfg:  &Config{},
-			err:  notFoundError("certificate", "x"),
+			cfg: &Config{
+				err: notFoundError("certificate", "x"),
+			},
 		},
 		{
 			name: `Lifetime(10ms)`,
@@ -359,8 +359,9 @@ func TestOptions(t *testing.T) {
 		{
 			name: `PrivateKeyFile() error`,
 			opt:  PrivateKeyFile("x"),
-			cfg:  &Config{},
-			err:  notFoundError("private key", "x"),
+			cfg: &Config{
+				err: notFoundError("private key", "x"),
+			},
 		},
 		{
 			name: `ProductURI("a")`,
@@ -431,8 +432,9 @@ func TestOptions(t *testing.T) {
 		{
 			name: `RemoteCertificateFile() error`,
 			opt:  RemoteCertificateFile("x"),
-			cfg:  &Config{},
-			err:  notFoundError("certificate", "x"),
+			cfg: &Config{
+				err: notFoundError("certificate", "x"),
+			},
 		},
 		{
 			name: `RequestTimeout(5s)`,
@@ -801,8 +803,8 @@ func TestOptions(t *testing.T) {
 				return ""
 			}
 
-			cfg, err := ApplyConfig(tt.opt)
-			if got, want := errstr(err), errstr(tt.err); got != "" || want != "" {
+			cfg := ApplyConfig(tt.opt)
+			if got, want := errstr(cfg.Error()), errstr(tt.cfg.err); got != "" || want != "" {
 				if got != want {
 					t.Fatalf("got error %q want %q", got, want)
 				}
diff --git a/uatest/method_test.go b/uatest/method_test.go
index 7717271..ea0feba 100644
--- a/uatest/method_test.go
+++ b/uatest/method_test.go
@@ -61,10 +61,7 @@ func TestCallMethod(t *testing.T) {
 	srv := NewServer("method_server.py")
 	defer srv.Close()
 
-	c, err := opcua.NewClient(srv.Endpoint, srv.Opts...)
-	if err != nil {
-		t.Fatal(err)
-	}
+	c := opcua.NewClient(srv.Endpoint, srv.Opts...)
 	if err := c.Connect(ctx); err != nil {
 		t.Fatal(err)
 	}
diff --git a/uatest/namespace_test.go b/uatest/namespace_test.go
index ddec547..ca53f9c 100644
--- a/uatest/namespace_test.go
+++ b/uatest/namespace_test.go
@@ -18,10 +18,7 @@ func TestNamespace(t *testing.T) {
 	srv := NewServer("rw_server.py")
 	defer srv.Close()
 
-	c, err := opcua.NewClient(srv.Endpoint, srv.Opts...)
-	if err != nil {
-		t.Fatal(err)
-	}
+	c := opcua.NewClient(srv.Endpoint, srv.Opts...)
 	if err := c.Connect(ctx); err != nil {
 		t.Fatal(err)
 	}
diff --git a/uatest/read_test.go b/uatest/read_test.go
index b23c46c..f425aa1 100644
--- a/uatest/read_test.go
+++ b/uatest/read_test.go
@@ -33,10 +33,7 @@ func TestRead(t *testing.T) {
 	srv := NewServer("rw_server.py")
 	defer srv.Close()
 
-	c, err := opcua.NewClient(srv.Endpoint, srv.Opts...)
-	if err != nil {
-		t.Fatal(err)
-	}
+	c := opcua.NewClient(srv.Endpoint, srv.Opts...)
 	if err := c.Connect(ctx); err != nil {
 		t.Fatal(err)
 	}
diff --git a/uatest/read_unknow_node_id_test.go b/uatest/read_unknow_node_id_test.go
index b5fd424..a729f98 100644
--- a/uatest/read_unknow_node_id_test.go
+++ b/uatest/read_unknow_node_id_test.go
@@ -20,10 +20,7 @@ func TestReadUnknowNodeID(t *testing.T) {
 	srv := NewServer("read_unknow_node_id_server.py")
 	defer srv.Close()
 
-	c, err := opcua.NewClient(srv.Endpoint, srv.Opts...)
-	if err != nil {
-		t.Fatal(err)
-	}
+	c := opcua.NewClient(srv.Endpoint, srv.Opts...)
 	if err := c.Connect(ctx); err != nil {
 		t.Fatal(err)
 	}
diff --git a/uatest/reconnection_test.go b/uatest/reconnection_test.go
index e919c53..f6d2050 100644
--- a/uatest/reconnection_test.go
+++ b/uatest/reconnection_test.go
@@ -27,10 +27,7 @@ func TestAutoReconnection(t *testing.T) {
 	srv := NewServer("reconnection_server.py")
 	defer srv.Close()
 
-	c, err := opcua.NewClient(srv.Endpoint, srv.Opts...)
-	if err != nil {
-		t.Fatal(err)
-	}
+	c := opcua.NewClient(srv.Endpoint, srv.Opts...)
 	if err := c.Connect(ctx); err != nil {
 		t.Fatal(err)
 	}
diff --git a/uatest/stats_test.go b/uatest/stats_test.go
index f15bf33..070a351 100644
--- a/uatest/stats_test.go
+++ b/uatest/stats_test.go
@@ -28,10 +28,7 @@ func TestStats(t *testing.T) {
 	srv := NewServer("rw_server.py")
 	defer srv.Close()
 
-	c, err := opcua.NewClient(srv.Endpoint, srv.Opts...)
-	if err != nil {
-		t.Fatal(err)
-	}
+	c := opcua.NewClient(srv.Endpoint, srv.Opts...)
 	if err := c.Connect(ctx); err != nil {
 		t.Fatal(err)
 	}
diff --git a/uatest/timeout_test.go b/uatest/timeout_test.go
index 8a83c96..0d29eba 100644
--- a/uatest/timeout_test.go
+++ b/uatest/timeout_test.go
@@ -21,19 +21,13 @@ const (
 )
 
 func TestClientTimeoutViaOptions(t *testing.T) {
-	c, err := opcua.NewClient(tcpNoRstTestServer, opcua.DialTimeout(forceTimeoutDuration))
-	if err != nil {
-		t.Fatal(err)
-	}
+	c := opcua.NewClient(tcpNoRstTestServer, opcua.DialTimeout(forceTimeoutDuration))
 
 	connectAndValidate(t, c, context.Background(), forceTimeoutDuration)
 }
 
 func TestClientTimeoutViaContext(t *testing.T) {
-	c, err := opcua.NewClient(tcpNoRstTestServer)
-	if err != nil {
-		t.Fatal(err)
-	}
+	c := opcua.NewClient(tcpNoRstTestServer)
 
 	ctx, cancel := context.WithTimeout(context.Background(), forceTimeoutDuration)
 	defer cancel()
diff --git a/uatest/write_test.go b/uatest/write_test.go
index f1b76b7..b6c0d1c 100644
--- a/uatest/write_test.go
+++ b/uatest/write_test.go
@@ -32,10 +32,7 @@ func TestWrite(t *testing.T) {
 	srv := NewServer("rw_server.py")
 	defer srv.Close()
 
-	c, err := opcua.NewClient(srv.Endpoint, srv.Opts...)
-	if err != nil {
-		t.Fatal(err)
-	}
+	c := opcua.NewClient(srv.Endpoint, srv.Opts...)
 	if err := c.Connect(ctx); err != nil {
 		t.Fatal(err)
 	}