Repository: gopcua/opcua
Pull request #674 (2023-08-21 07:46:03)

refactor: make NewClient return an error
This PR addresses the TODO `Note: Starting with v0.5 this function will return an error.` and let NewClient return an error.

Note on the PR: I use `github.com/stretchr/testify` for error assertion with seems IMHO pretty standard in the go projects I worked with. If this is unwanted I could rework this.

resolves https://github.com/gopcua/opcua/issues/621

sruehl: @magiconair I worked on a TODO you defined in source. Not sure if there is a ticket for that. If I find it I link it here.
sruehl: Oh and before I forget, take a closer look (@magiconair) what I did with `errors`. So I collect the errors there and return them with join. Could also be done via early exit. Also I wrap errors with `%w`. Maybe this is unwanted too.
sruehl: > I'll have a look. As a guiding principle I prefer consistency when we make changes like this. I'm OK with changing direction but then let's do it all over the place. And in a separate PR.

Clarifying question on that: I assumed that comment was related to wrapping with %w. Should I switch from errrors join to early exit too?
magiconair: As an aside: I've done the force-push update as well earlier in my career and while that keeps the commit cleaner it makes reviewing harder since I don't see what you have changed and always have to review the full change set. I've come to settle on individual commits and leave it to the maintainer to squash the commits if they want. 

Patch:
diff --git a/.gitignore b/.gitignore
index 6a9856f..971ffc6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ build/
 dist/
 __pycache__/
 .vscode/
+.idea/
diff --git a/client.go b/client.go
index 17cadd2..67bbb0a 100644
--- a/client.go
+++ b/client.go
@@ -30,7 +30,10 @@ 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 := NewClient(endpoint, opts...)
+	c, err := NewClient(endpoint, opts...)
+	if err != nil {
+		return nil, err
+	}
 	if err := c.Dial(ctx); err != nil {
 		return nil, err
 	}
@@ -133,15 +136,6 @@ 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.
@@ -155,10 +149,11 @@ type Client struct {
 // #Option for details.
 //
 // https://godoc.org/github.com/gopcua/opcua#Option
-//
-// Note: Starting with v0.5 this function will return an error.
-func NewClient(endpoint string, opts ...Option) *Client {
-	cfg := ApplyConfig(opts...)
+func NewClient(endpoint string, opts ...Option) (*Client, error) {
+	cfg, err := ApplyConfig(opts...)
+	if err != nil {
+		return nil, err
+	}
 	c := Client{
 		endpointURL: endpoint,
 		cfg:         cfg,
@@ -167,7 +162,6 @@ func NewClient(endpoint string, opts ...Option) *Client {
 		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)
@@ -175,7 +169,7 @@ func NewClient(endpoint string, opts ...Option) *Client {
 	c.setSecureChannel(nil)
 	c.setSession(nil)
 	c.setNamespaces([]string{})
-	return &c
+	return &c, nil
 }
 
 // reconnectAction is a list of actions for the client reconnection logic.
@@ -194,11 +188,6 @@ 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
@@ -503,16 +492,16 @@ func (c *Client) monitor(ctx context.Context) {
 						// populated in the previous step.
 
 						activeSubs = 0
-						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)
+						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)
 							}
 							activeSubs++
 						}
 
-						for _, id := range subsToRecreate {
-							if err := c.recreateSubscription(ctx, id); err != nil {
+						for _, subID := range subsToRecreate {
+							if err := c.recreateSubscription(ctx, subID); err != nil {
 								dlog.Printf("recreate subscripitions failed: %v", err)
 								action = recreateSession
 								continue
@@ -555,11 +544,6 @@ 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 {
@@ -754,10 +738,16 @@ 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 fd92a31..ff42433 100644
--- a/config.go
+++ b/config.go
@@ -56,18 +56,6 @@ 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
@@ -80,68 +68,76 @@ func NewDialer(cfg *Config) *uacp.Dialer {
 
 // ApplyConfig applies the config options to the default configuration.
 // todo(fs): Can we find a better name?
-//
-// Note: Starting with v0.5 this function will return an error.
-func ApplyConfig(opts ...Option) *Config {
+func ApplyConfig(opts ...Option) (*Config, error) {
 	cfg := &Config{
 		sechan:  DefaultClientConfig(),
 		session: DefaultSessionConfig(),
 	}
+	var errs []error
 	for _, opt := range opts {
-		opt(cfg)
+		if err := opt(cfg); err != nil {
+			errs = append(errs, err)
+		}
 	}
-	return cfg
+	return cfg, errors.Join(errs...)
 }
 
 // Option is an option function type to modify the configuration.
-type Option func(*Config)
+type Option func(*Config) error
 
 // ApplicationName sets the application name in the session configuration.
 func ApplicationName(s string) Option {
-	return func(cfg *Config) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		cfg.sechan.AutoReconnect = b
+		return nil
 	}
 }
 
 // ReconnectInterval is interval duration between each reconnection attempt.
 func ReconnectInterval(d time.Duration) Option {
-	return func(cfg *Config) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		cfg.session.LocaleIDs = locale
+		return nil
 	}
 }
 
 // ProductURI sets the product uri in the session configuration.
 func ProductURI(s string) Option {
-	return func(cfg *Config) {
+	return func(cfg *Config) error {
 		cfg.session.ClientDescription.ProductURI = s
+		return nil
 	}
 }
 
@@ -154,95 +150,103 @@ var randomRequestID func() uint32 = nil
 // is the caller's responsibility to initialize the random number
 // generator properly.
 func RandomRequestID() Option {
-	return func(cfg *Config) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		if filename == "" {
-			return
+			return nil
 		}
 
 		cert, err := loadCertificate(filename)
 		if err != nil {
-			cfg.setError(err)
-			return
+			return err
 		}
 		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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		cfg.session.SessionName = s
+		return nil
 	}
 }
 
 // SessionTimeout sets the timeout in the session configuration.
 func SessionTimeout(d time.Duration) Option {
-	return func(cfg *Config) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		if filename == "" {
-			return
+			return nil
 		}
 		key, err := loadPrivateKey(filename)
 		if err != nil {
-			cfg.setError(err)
-			return
+			return err
 		}
 		cfg.sechan.LocalKey = key
+		return nil
 	}
 }
 
@@ -271,26 +275,27 @@ 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) {
+	return func(cfg *Config) error {
 		setCertificate(cert, cfg)
+		return nil
 	}
 }
 
-// Certificate sets the client X509 certificate in the secure channel configuration
+// CertificateFile 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) {
+	return func(cfg *Config) error {
 		if filename == "" {
-			return
+			return nil
 		}
 
 		cert, err := loadCertificate(filename)
 		if err != nil {
-			cfg.setError(err)
-			return
+			return err
 		}
 		setCertificate(cert, cfg)
+		return nil
 	}
 }
 
@@ -333,7 +338,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) {
+	return func(cfg *Config) error {
 		cfg.sechan.SecurityPolicyURI = ep.SecurityPolicyURI
 		cfg.sechan.SecurityMode = ep.SecurityMode
 		cfg.sechan.RemoteCertificate = ep.ServerCertificate
@@ -359,13 +364,14 @@ func SecurityFromEndpoint(ep *ua.EndpointDescription, authType ua.UserTokenType)
 
 			setPolicyID(cfg.session.UserIdentityToken, t.PolicyID)
 			cfg.session.AuthPolicyURI = t.SecurityPolicyURI
-			return
+			return nil
 		}
 
 		if cfg.session.UserIdentityToken == nil {
 			cfg.session.UserIdentityToken = &ua.AnonymousIdentityToken{PolicyID: defaultAnonymousPolicyID}
 			cfg.session.AuthPolicyURI = ua.SecurityPolicyURINone
 		}
+		return nil
 	}
 }
 
@@ -389,12 +395,13 @@ 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) {
+	return func(cfg *Config) error {
 		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
+			return nil
 		}
 		setPolicyID(cfg.session.UserIdentityToken, policy)
+		return nil
 	}
 }
 
@@ -402,7 +409,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) {
+	return func(cfg *Config) error {
 		if cfg.session.UserIdentityToken == nil {
 			cfg.session.UserIdentityToken = &ua.AnonymousIdentityToken{}
 		}
@@ -411,8 +418,9 @@ func AuthAnonymous() Option {
 		if !ok {
 			// todo(fs): should we Fatal here?
 			log.Printf("non-anonymous authentication already configured, ignoring")
-			return
+			return nil
 		}
+		return nil
 	}
 }
 
@@ -420,7 +428,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) {
+	return func(cfg *Config) error {
 		if cfg.session.UserIdentityToken == nil {
 			cfg.session.UserIdentityToken = &ua.UserNameIdentityToken{}
 		}
@@ -429,11 +437,12 @@ func AuthUsername(user, pass string) Option {
 		if !ok {
 			// todo(fs): should we Fatal here?
 			log.Printf("non-username authentication already configured, ignoring")
-			return
+			return nil
 		}
 
 		t.UserName = user
 		cfg.session.AuthPassword = pass
+		return nil
 	}
 }
 
@@ -441,7 +450,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) {
+	return func(cfg *Config) error {
 		if cfg.session.UserIdentityToken == nil {
 			cfg.session.UserIdentityToken = &ua.X509IdentityToken{}
 		}
@@ -450,10 +459,11 @@ func AuthCertificate(cert []byte) Option {
 		if !ok {
 			// todo(fs): should we Fatal here?
 			log.Printf("non-certificate authentication already configured, ignoring")
-			return
+			return nil
 		}
 
 		t.CertificateData = cert
+		return nil
 	}
 }
 
@@ -461,7 +471,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) {
+	return func(cfg *Config) error {
 		if cfg.session.UserIdentityToken == nil {
 			cfg.session.UserIdentityToken = &ua.IssuedIdentityToken{}
 		}
@@ -469,67 +479,75 @@ func AuthIssuedToken(tokenData []byte) Option {
 		t, ok := cfg.session.UserIdentityToken.(*ua.IssuedIdentityToken)
 		if !ok {
 			log.Printf("non-issued token authentication already configured, ignoring")
-			return
+			return nil
 		}
 
 		// 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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		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) {
+	return func(cfg *Config) error {
 		initDialer(cfg)
 		cfg.dialer.ClientACK.SendBufSize = n
+		return nil
 	}
 }
 
diff --git a/errors/errors.go b/errors/errors.go
index bfc84da..7280f35 100644
--- a/errors/errors.go
+++ b/errors/errors.go
@@ -34,6 +34,11 @@ 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 39bc234..a6ee453 100644
--- a/examples/accesslevel/accesslevel.go
+++ b/examples/accesslevel/accesslevel.go
@@ -25,7 +25,10 @@ func main() {
 
 	ctx := context.Background()
 
-	c := opcua.NewClient(*endpoint)
+	c, err := opcua.NewClient(*endpoint)
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/browse/browse.go b/examples/browse/browse.go
index 2287662..27826b2 100644
--- a/examples/browse/browse.go
+++ b/examples/browse/browse.go
@@ -174,7 +174,10 @@ func main() {
 
 	ctx := context.Background()
 
-	c := opcua.NewClient(*endpoint)
+	c, err := opcua.NewClient(*endpoint)
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/crypto/crypto.go b/examples/crypto/crypto.go
index dc7b620..76b92d9 100644
--- a/examples/crypto/crypto.go
+++ b/examples/crypto/crypto.go
@@ -63,7 +63,10 @@ func main() {
 	opts := clientOptsFromFlags(endpoints)
 
 	// Create a Client with the selected options
-	c := opcua.NewClient(*endpoint, opts...)
+	c, err := opcua.NewClient(*endpoint, opts...)
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
@@ -86,7 +89,10 @@ func main() {
 		log.Fatalf("Error detaching session: %s", err)
 	}
 
-	d := opcua.NewClient(*endpoint, opts...)
+	d, err := opcua.NewClient(*endpoint, opts...)
+	if err != nil {
+		log.Fatal(err)
+	}
 
 	// 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 ad2ae77..dca8063 100644
--- a/examples/datetime/datetime.go
+++ b/examples/datetime/datetime.go
@@ -47,7 +47,10 @@ func main() {
 		opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous),
 	}
 
-	c := opcua.NewClient(ep.EndpointURL, opts...)
+	c, err := opcua.NewClient(ep.EndpointURL, opts...)
+	if err != nil {
+		log.Fatal(err)
+	}
 	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 d3cc2b5..c0ca3c0 100644
--- a/examples/history-read/history-read.go
+++ b/examples/history-read/history-read.go
@@ -24,7 +24,10 @@ func main() {
 
 	ctx := context.Background()
 
-	c := opcua.NewClient(*endpoint)
+	c, err := opcua.NewClient(*endpoint)
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/method/method.go b/examples/method/method.go
index 802dcd7..7a16585 100644
--- a/examples/method/method.go
+++ b/examples/method/method.go
@@ -24,7 +24,10 @@ func main() {
 
 	ctx := context.Background()
 
-	c := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
+	c, err := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/monitor/monitor.go b/examples/monitor/monitor.go
index 8700cbe..ef0f39c 100644
--- a/examples/monitor/monitor.go
+++ b/examples/monitor/monitor.go
@@ -63,7 +63,10 @@ func main() {
 		opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous),
 	}
 
-	c := opcua.NewClient(ep.EndpointURL, opts...)
+	c, err := opcua.NewClient(ep.EndpointURL, opts...)
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/read/read.go b/examples/read/read.go
index a921e02..e33763f 100644
--- a/examples/read/read.go
+++ b/examples/read/read.go
@@ -28,7 +28,10 @@ func main() {
 
 	ctx := context.Background()
 
-	c := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
+	c, err := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/regread/regread.go b/examples/regread/regread.go
index ca2ca8b..01eb013 100644
--- a/examples/regread/regread.go
+++ b/examples/regread/regread.go
@@ -25,7 +25,10 @@ func main() {
 
 	ctx := context.Background()
 
-	c := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
+	c, err := opcua.NewClient(*endpoint, opcua.SecurityMode(ua.MessageSecurityModeNone))
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/subscribe/subscribe.go b/examples/subscribe/subscribe.go
index 32134d2..8127cd9 100644
--- a/examples/subscribe/subscribe.go
+++ b/examples/subscribe/subscribe.go
@@ -60,7 +60,10 @@ func main() {
 		opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous),
 	}
 
-	c := opcua.NewClient(ep.EndpointURL, opts...)
+	c, err := opcua.NewClient(ep.EndpointURL, opts...)
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/translate/translate.go b/examples/translate/translate.go
index 3f9c6ff..48415a0 100644
--- a/examples/translate/translate.go
+++ b/examples/translate/translate.go
@@ -27,7 +27,10 @@ func main() {
 
 	ctx := context.Background()
 
-	c := opcua.NewClient(*endpoint)
+	c, err := opcua.NewClient(*endpoint)
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/trigger/trigger.go b/examples/trigger/trigger.go
index e6f685f..beafb3b 100644
--- a/examples/trigger/trigger.go
+++ b/examples/trigger/trigger.go
@@ -59,7 +59,10 @@ func main() {
 		opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous),
 	}
 
-	c := opcua.NewClient(ep.EndpointURL, opts...)
+	c, err := opcua.NewClient(ep.EndpointURL, opts...)
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/udt/udt.go b/examples/udt/udt.go
index b7eebdd..6fa3ad5 100644
--- a/examples/udt/udt.go
+++ b/examples/udt/udt.go
@@ -55,7 +55,10 @@ func main() {
 		opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous),
 	}
 
-	c := opcua.NewClient(ep.EndpointURL, opts...)
+	c, err := opcua.NewClient(ep.EndpointURL, opts...)
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/write/write.go b/examples/write/write.go
index 62f9dd9..ca97d24 100644
--- a/examples/write/write.go
+++ b/examples/write/write.go
@@ -26,7 +26,10 @@ func main() {
 
 	ctx := context.Background()
 
-	c := opcua.NewClient(*endpoint)
+	c, err := opcua.NewClient(*endpoint)
+	if err != nil {
+		log.Fatal(err)
+	}
 	if err := c.Connect(ctx); err != nil {
 		log.Fatal(err)
 	}


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