Repository: ably/ably-go
Pull request #199 (2020-10-07 08:07:14)

Introduce ErrorCode type for predefined error codes
This way it's easier to refer to them in godoc and generally in static analysis.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 9a3e5f7..45e94e4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,4 +9,6 @@ env:
   - GOMAXPROCS=4 GO111MODULE=on
 sudo: false
 script:
+  - make build
+  - if ! (git status | grep "working tree clean"); then echo "uncommitted generated code; run 'make build' and commit the result"; exit 1; fi
   - make test
diff --git a/Makefile b/Makefile
index 053acad..55ffafd 100644
--- a/Makefile
+++ b/Makefile
@@ -4,6 +4,7 @@ vet:
 	go vet ./ably ./scripts
 
 build:
+	go generate ./...
 	go build ./...
 
 test: submodules
diff --git a/ably/auth.go b/ably/auth.go
index 6701758..18b94d6 100644
--- a/ably/auth.go
+++ b/ably/auth.go
@@ -449,7 +449,7 @@ func (a *Auth) isTokenRenewable() bool {
 	return a.opts().Key != "" || a.opts().AuthURL != "" || a.opts().AuthCallback != nil
 }
 
-func (a *Auth) newError(code int, err error) error {
+func (a *Auth) newError(code ErrorCode, err error) error {
 	return newError(code, err)
 }
 
diff --git a/ably/error.go b/ably/error.go
index 08fba4c..57a857b 100644
--- a/ably/error.go
+++ b/ably/error.go
@@ -12,21 +12,42 @@ import (
 	"github.com/ably/ably-go/ably/proto"
 )
 
-func toStatusCode(code int) int {
-	switch status := code / 100; status {
-	case 400, 401, 403, 404, 405, 500:
+func (code ErrorCode) toStatusCode() int {
+	switch status := int(code) / 100; status {
+	case
+		http.StatusBadRequest,
+		http.StatusUnauthorized,
+		http.StatusForbidden,
+		http.StatusNotFound,
+		http.StatusMethodNotAllowed,
+		http.StatusInternalServerError:
 		return status
 	default:
 		return 0
 	}
 }
 
+func codeFromStatus(statusCode int) ErrorCode {
+	switch code := ErrorCode(statusCode * 100); code {
+	case
+		ErrBadRequest,
+		ErrUnauthorized,
+		ErrForbidden,
+		ErrNotFound,
+		ErrMethodNotAllowed,
+		ErrInternalError:
+		return code
+	default:
+		return ErrNotSet
+	}
+}
+
 // ErrorInfo describes error returned from Ably API. It always has non-zero error
 // code. It may contain underlying error value which caused the failure
 // condition.
 type ErrorInfo struct {
 	StatusCode int
-	Code       int
+	Code       ErrorCode
 	HRef       string
 	Cause      *ErrorInfo
 
@@ -44,9 +65,10 @@ func (e ErrorInfo) Error() string {
 	msg := e.Message()
 	var see string
 	if !strings.Contains(msg, errorHref) {
-		see = "See " + errorHref
+		see = " See " + errorHref
 	}
-	return fmt.Sprintf("[ErrorInfo :%s code=%d statusCode=%d] %s", msg, e.Code, e.StatusCode, see)
+	return fmt.Sprintf("[ErrorInfo :%s code=%d %[2]v statusCode=%d]%s", msg, e.Code, e.StatusCode, see)
+
 }
 
 // Unwrap implements the implicit interface that errors.Unwrap understands.
@@ -62,7 +84,7 @@ func (e ErrorInfo) Message() string {
 	return e.err.Error()
 }
 
-func newError(defaultCode int, err error) *ErrorInfo {
+func newError(defaultCode ErrorCode, err error) *ErrorInfo {
 	switch err := err.(type) {
 	case nil:
 		return nil
@@ -75,33 +97,33 @@ func newError(defaultCode int, err error) *ErrorInfo {
 	}
 	return &ErrorInfo{
 		Code:       defaultCode,
-		StatusCode: toStatusCode(defaultCode),
+		StatusCode: defaultCode.toStatusCode(),
 
 		err: err,
 	}
 }
 
-func newErrorf(code int, format string, v ...interface{}) *ErrorInfo {
+func newErrorf(code ErrorCode, format string, v ...interface{}) *ErrorInfo {
 	return newError(code, fmt.Errorf(format, v...))
 }
 
-func newErrorProto(err *proto.ErrorInfo) *ErrorInfo {
+func newErrorFromProto(err *proto.ErrorInfo) *ErrorInfo {
 	if err == nil {
 		return nil
 	}
 	return &ErrorInfo{
-		Code:       err.Code,
+		Code:       ErrorCode(err.Code),
 		StatusCode: err.StatusCode,
 		HRef:       err.HRef,
 		err:        errors.New(err.Message),
 	}
 }
 
-func code(err error) int {
+func code(err error) ErrorCode {
 	if e, ok := err.(*ErrorInfo); ok {
 		return e.Code
 	}
-	return 0
+	return ErrNotSet
 }
 
 func errFromUnprocessableBody(resp *http.Response) error {
@@ -109,7 +131,7 @@ func errFromUnprocessableBody(resp *http.Response) error {
 	if err == nil {
 		err = errors.New(string(errMsg))
 	}
-	return &ErrorInfo{Code: 40000, StatusCode: resp.StatusCode, err: err}
+	return &ErrorInfo{Code: ErrBadRequest, StatusCode: resp.StatusCode, err: err}
 }
 
 func checkValidHTTPResponse(resp *http.Response) error {
@@ -134,18 +156,15 @@ func checkValidHTTPResponse(resp *http.Response) error {
 
 	body := &errorBody{}
 	if err := decode(typ, resp.Body, &body); err != nil {
-		return newError(resp.StatusCode*100, errors.New(http.StatusText(resp.StatusCode)))
+		return newError(codeFromStatus(resp.StatusCode), errors.New(http.StatusText(resp.StatusCode)))
 	}
 
-	err := &ErrorInfo{
-		Code:       body.Error.Code,
-		StatusCode: body.Error.StatusCode,
-	}
+	err := newErrorFromProto(&body.Error)
 	if body.Error.Message != "" {
 		err.err = errors.New(body.Error.Message)
 	}
 	if err.Code == 0 && err.StatusCode == 0 {
-		err.Code, err.StatusCode = resp.StatusCode*100, resp.StatusCode
+		err.Code, err.StatusCode = codeFromStatus(resp.StatusCode), resp.StatusCode
 	}
 	return err
 }
diff --git a/ably/errors.go b/ably/errors.go
index 5748a1e..b15b305 100644
--- a/ably/errors.go
+++ b/ably/errors.go
@@ -1,240 +1,363 @@
-// Code generated by errors-const v0.1.0 tool DO NOT EDIT.
+// Code generated by errors-const v0.2.0 tool DO NOT EDIT.
 
 package ably
 
-// ably error codes
+// ErrorCode is the type for predefined Ably error codes.
+type ErrorCode int
+
 const (
-	ErrNoError                                                                 = 10000
-	ErrBadRequest                                                              = 40000
-	ErrInvalidRequestBody                                                      = 40001
-	ErrInvalidParameterName                                                    = 40002
-	ErrInvalidParameterValue                                                   = 40003
-	ErrInvalidHeader                                                           = 40004
-	ErrInvalidCredential                                                       = 40005
-	ErrInvalidConnectionID                                                     = 40006
-	ErrInvalidMessageID                                                        = 40007
-	ErrInvalidContentLength                                                    = 40008
-	ErrMaximumMessageLengthExceeded                                            = 40009
-	ErrInvalidChannelName                                                      = 40010
-	ErrStaleRingState                                                          = 40011
-	ErrInvalidClientID                                                         = 40012
-	ErrInvalidMessageDataOrEncoding                                            = 40013
-	ErrResourceDisposed                                                        = 40014
-	ErrInvalidDeviceID                                                         = 40015
-	ErrBatchError                                                              = 40020
-	ErrInvalidPublishRequestUnspecified                                        = 40030
-	ErrInvalidPublishRequestInvalidClientSpecifiedID                           = 40031
-	ErrUnauthorized                                                            = 40100
-	ErrInvalidCredentials                                                      = 40101
-	ErrIncompatibleCredentials                                                 = 40102
-	ErrInvalidUseOfBasicAuthOverNonTLSTransport                                = 40103
-	ErrTimestampNotCurrent                                                     = 40104
-	ErrNonceValueReplayed                                                      = 40105
-	ErrUnableToObtainCredentialsFromGivenParameters                            = 40106
-	ErrAccountDisabled                                                         = 40110
-	ErrAccountRestrictedConnectionLimitsExceeded                               = 40111
-	ErrAccountBlockedMessageLimitsExceeded                                     = 40112
-	ErrAccountBlocked                                                          = 40113
-	ErrAccountRestrictedChannelLimitsExceeded                                  = 40114
-	ErrApplicationDisabled                                                     = 40120
-	ErrKeyErrorUnspecified                                                     = 40130
-	ErrKeyRevoked                                                              = 40131
-	ErrKeyExpired                                                              = 40132
-	ErrKeyDisabled                                                             = 40133
-	ErrTokenErrorUnspecified                                                   = 40140
-	ErrTokenRevoked                                                            = 40141
-	ErrTokenExpired                                                            = 40142
-	ErrTokenUnrecognised                                                       = 40143
-	ErrInvalidJWTFormat                                                        = 40144
-	ErrInvalidTokenFormat                                                      = 40145
-	ErrConnectionBlockedLimitsExceeded                                         = 40150
-	ErrOperationNotPermittedWithProvidedCapability                             = 40160
-	ErrErrorFromClientTokenCallback                                            = 40170
-	ErrForbidden                                                               = 40300
-	ErrAccountDoesNotPermitTLSConnection                                       = 40310
-	ErrOperationRequiresTLSConnection                                          = 40311
-	ErrApplicationRequiresAuthentication                                       = 40320
-	ErrNotFound                                                                = 40400
-	ErrMethodNotAllowed                                                        = 40500
-	ErrRateLimitExceededNonfatal                                               = 42910
-	ErrMaxPerConnectionPublishRateLimitExceededNonfatal                        = 42911
-	ErrRateLimitExceededFatal                                                  = 42920
-	ErrMaxPerConnectionPublishRateLimitExceededFatal                           = 42921
-	ErrInternalError                                                           = 50000
-	ErrInternalChannelError                                                    = 50001
-	ErrInternalConnectionError                                                 = 50002
-	ErrTimeoutError                                                            = 50003
-	ErrRequestFailedDueToOverloadedInstance                                    = 50004
-	ErrReactorOperationFailed                                                  = 70000
-	ErrReactorOperationFailedPostOperationFailed                               = 70001
-	ErrReactorOperationFailedPostOperationReturnedUnexpectedCode               = 70002
-	ErrReactorOperationFailedMaximumNumberOfConcurrentInFlightRequestsExceeded = 70003
-	ErrExchangeErrorUnspecified                                                = 71000
-	ErrForcedReAttachmentDueToPermissionsChange                                = 71001
-	ErrExchangePublisherErrorUnspecified                                       = 71100
-	ErrNoSuchPublisher                                                         = 71101
-	ErrPublisherNotEnabledAsAnExchangePublisher                                = 71102
-	ErrExchangeProductErrorUnspecified                                         = 71200
-	ErrNoSuchProduct                                                           = 71201
-	ErrProductDisabled                                                         = 71202
-	ErrNoSuchChannelInThisProduct                                              = 71203
-	ErrExchangeSubscriptionErrorUnspecified                                    = 71300
-	ErrSubscriptionDisabled                                                    = 71301
-	ErrRequesterHasNoSubscriptionToThisProduct                                 = 71302
-	ErrConnectionFailed                                                        = 80000
-	ErrConnectionFailedNoCompatibleTransport                                   = 80001
-	ErrConnectionSuspended                                                     = 80002
-	ErrDisconnected                                                            = 80003
-	ErrAlreadyConnected                                                        = 80004
-	ErrInvalidConnectionIDRemoteNotFound                                       = 80005
-	ErrUnableToRecoverConnectionMessagesExpired                                = 80006
-	ErrUnableToRecoverConnectionMessageLimitExceeded                           = 80007
-	ErrUnableToRecoverConnectionConnectionExpired                              = 80008
-	ErrConnectionNotEstablishedNoTransportHandle                               = 80009
-	ErrInvalidOperationInvalidTransportHandle                                  = 80010
-	ErrUnableToRecoverConnectionIncompatibleAuthParams                         = 80011
-	ErrUnableToRecoverConnectionInvalidOrUnspecifiedConnectionSerial           = 80012
-	ErrProtocolError                                                           = 80013
-	ErrConnectionTimedOut                                                      = 80014
-	ErrIncompatibleConnectionParameters                                        = 80015
-	ErrOperationOnSupersededTransport                                          = 80016
-	ErrConnectionClosed                                                        = 80017
-	ErrInvalidConnectionIDInvalidFormat                                        = 80018
-	ErrClientConfiguredAuthenticationProviderRequestFailed                     = 80019
-	ErrContinuityLossDueToMaximumSubscribeMessageRateExceeded                  = 80020
-	ErrClientRestrictionNotSatisfied                                           = 80030
-	ErrChannelOperationFailed                                                  = 90000
-	ErrChannelOperationFailedInvalidChannelState                               = 90001
-	ErrChannelOperationFailedEpochExpiredOrNeverExisted                        = 90002
-	ErrUnableToRecoverChannelMessagesExpired                                   = 90003
-	ErrUnableToRecoverChannelMessageLimitExceeded                              = 90004
-	ErrUnableToRecoverChannelNoMatchingEpoch                                   = 90005
-	ErrUnableToRecoverChannelUnboundedRequest                                  = 90006
-	ErrChannelOperationFailedNoResponseFromServer                              = 90007
-	ErrMaximumNumberOfChannelsPerConnectionExceeded                            = 90010
-	ErrUnableToEnterPresenceChannelNoClientID                                  = 91000
-	ErrUnableToEnterPresenceChannelInvalidChannelState                         = 91001
-	ErrUnableToLeavePresenceChannelThatIsNotEntered                            = 91002
-	ErrUnableToEnterPresenceChannelMaximumMemberLimitExceeded                  = 91003
-	ErrUnableToAutomaticallyReEnterPresenceChannel                             = 91004
-	ErrPresenceStateIsOutOfSync                                                = 91005
-	ErrMemberImplicitlyLeftPresenceChannelConnectionClosed                     = 91100
+	ErrNotSet                                                                  ErrorCode = 0
+	ErrNoError                                                                 ErrorCode = 10000
+	ErrBadRequest                                                              ErrorCode = 40000
+	ErrInvalidRequestBody                                                      ErrorCode = 40001
+	ErrInvalidParameterName                                                    ErrorCode = 40002
+	ErrInvalidParameterValue                                                   ErrorCode = 40003
+	ErrInvalidHeader                                                           ErrorCode = 40004
+	ErrInvalidCredential                                                       ErrorCode = 40005
+	ErrInvalidConnectionID                                                     ErrorCode = 40006
+	ErrInvalidMessageID                                                        ErrorCode = 40007
+	ErrInvalidContentLength                                                    ErrorCode = 40008
+	ErrMaximumMessageLengthExceeded                                            ErrorCode = 40009
+	ErrInvalidChannelName                                                      ErrorCode = 40010
+	ErrStaleRingState                                                          ErrorCode = 40011
+	ErrInvalidClientID                                                         ErrorCode = 40012
+	ErrInvalidMessageDataOrEncoding                                            ErrorCode = 40013
+	ErrResourceDisposed                                                        ErrorCode = 40014
+	ErrInvalidDeviceID                                                         ErrorCode = 40015
+	ErrBatchError                                                              ErrorCode = 40020
+	ErrInvalidPublishRequestUnspecified                                        ErrorCode = 40030
+	ErrInvalidPublishRequestInvalidClientSpecifiedID                           ErrorCode = 40031
+	ErrUnauthorized                                                            ErrorCode = 40100
+	ErrInvalidCredentials                                                      ErrorCode = 40101
+	ErrIncompatibleCredentials                                                 ErrorCode = 40102
+	ErrInvalidUseOfBasicAuthOverNonTLSTransport                                ErrorCode = 40103
+	ErrTimestampNotCurrent                                                     ErrorCode = 40104
+	ErrNonceValueReplayed                                                      ErrorCode = 40105
+	ErrUnableToObtainCredentialsFromGivenParameters                            ErrorCode = 40106
+	ErrAccountDisabled                                                         ErrorCode = 40110
+	ErrAccountRestrictedConnectionLimitsExceeded                               ErrorCode = 40111
+	ErrAccountBlockedMessageLimitsExceeded                                     ErrorCode = 40112
+	ErrAccountBlocked                                                          ErrorCode = 40113
+	ErrAccountRestrictedChannelLimitsExceeded                                  ErrorCode = 40114
+	ErrApplicationDisabled                                                     ErrorCode = 40120
+	ErrKeyErrorUnspecified                                                     ErrorCode = 40130
+	ErrKeyRevoked                                                              ErrorCode = 40131
+	ErrKeyExpired                                                              ErrorCode = 40132
+	ErrKeyDisabled                                                             ErrorCode = 40133
+	ErrTokenErrorUnspecified                                                   ErrorCode = 40140
+	ErrTokenRevoked                                                            ErrorCode = 40141
+	ErrTokenExpired                                                            ErrorCode = 40142
+	ErrTokenUnrecognised                                                       ErrorCode = 40143
+	ErrInvalidJWTFormat                                                        ErrorCode = 40144
+	ErrInvalidTokenFormat                                                      ErrorCode = 40145
+	ErrConnectionBlockedLimitsExceeded                                         ErrorCode = 40150
+	ErrOperationNotPermittedWithProvidedCapability                             ErrorCode = 40160
+	ErrErrorFromClientTokenCallback                                            ErrorCode = 40170
+	ErrForbidden                                                               ErrorCode = 40300
+	ErrAccountDoesNotPermitTLSConnection                                       ErrorCode = 40310
+	ErrOperationRequiresTLSConnection                                          ErrorCode = 40311
+	ErrApplicationRequiresAuthentication                                       ErrorCode = 40320
+	ErrNotFound                                                                ErrorCode = 40400
+	ErrMethodNotAllowed                                                        ErrorCode = 40500
+	ErrRateLimitExceededNonfatal                                               ErrorCode = 42910
+	ErrMaxPerConnectionPublishRateLimitExceededNonfatal                        ErrorCode = 42911
+	ErrRateLimitExceededFatal                                                  ErrorCode = 42920
+	ErrMaxPerConnectionPublishRateLimitExceededFatal                           ErrorCode = 42921
+	ErrInternalError                                                           ErrorCode = 50000
+	ErrInternalChannelError                                                    ErrorCode = 50001
+	ErrInternalConnectionError                                                 ErrorCode = 50002
+	ErrTimeoutError                                                            ErrorCode = 50003
+	ErrRequestFailedDueToOverloadedInstance                                    ErrorCode = 50004
+	ErrReactorOperationFailed                                                  ErrorCode = 70000
+	ErrReactorOperationFailedPostOperationFailed                               ErrorCode = 70001
+	ErrReactorOperationFailedPostOperationReturnedUnexpectedCode               ErrorCode = 70002
+	ErrReactorOperationFailedMaximumNumberOfConcurrentInFlightRequestsExceeded ErrorCode = 70003
+	ErrExchangeErrorUnspecified                                                ErrorCode = 71000
+	ErrForcedReAttachmentDueToPermissionsChange                                ErrorCode = 71001
+	ErrExchangePublisherErrorUnspecified                                       ErrorCode = 71100
+	ErrNoSuchPublisher                                                         ErrorCode = 71101
+	ErrPublisherNotEnabledAsAnExchangePublisher                                ErrorCode = 71102
+	ErrExchangeProductErrorUnspecified                                         ErrorCode = 71200
+	ErrNoSuchProduct                                                           ErrorCode = 71201
+	ErrProductDisabled                                                         ErrorCode = 71202
+	ErrNoSuchChannelInThisProduct                                              ErrorCode = 71203
+	ErrExchangeSubscriptionErrorUnspecified                                    ErrorCode = 71300
+	ErrSubscriptionDisabled                                                    ErrorCode = 71301
+	ErrRequesterHasNoSubscriptionToThisProduct                                 ErrorCode = 71302
+	ErrConnectionFailed                                                        ErrorCode = 80000
+	ErrConnectionFailedNoCompatibleTransport                                   ErrorCode = 80001
+	ErrConnectionSuspended                                                     ErrorCode = 80002
+	ErrDisconnected                                                            ErrorCode = 80003
+	ErrAlreadyConnected                                                        ErrorCode = 80004
+	ErrInvalidConnectionIDRemoteNotFound                                       ErrorCode = 80005
+	ErrUnableToRecoverConnectionMessagesExpired                                ErrorCode = 80006
+	ErrUnableToRecoverConnectionMessageLimitExceeded                           ErrorCode = 80007
+	ErrUnableToRecoverConnectionConnectionExpired                              ErrorCode = 80008
+	ErrConnectionNotEstablishedNoTransportHandle                               ErrorCode = 80009
+	ErrInvalidOperationInvalidTransportHandle                                  ErrorCode = 80010
+	ErrUnableToRecoverConnectionIncompatibleAuthParams                         ErrorCode = 80011
+	ErrUnableToRecoverConnectionInvalidOrUnspecifiedConnectionSerial           ErrorCode = 80012
+	ErrProtocolError                                                           ErrorCode = 80013
+	ErrConnectionTimedOut                                                      ErrorCode = 80014
+	ErrIncompatibleConnectionParameters                                        ErrorCode = 80015
+	ErrOperationOnSupersededTransport                                          ErrorCode = 80016
+	ErrConnectionClosed                                                        ErrorCode = 80017
+	ErrInvalidConnectionIDInvalidFormat                                        ErrorCode = 80018
+	ErrClientConfiguredAuthenticationProviderRequestFailed                     ErrorCode = 80019
+	ErrContinuityLossDueToMaximumSubscribeMessageRateExceeded                  ErrorCode = 80020
+	ErrClientRestrictionNotSatisfied                                           ErrorCode = 80030
+	ErrChannelOperationFailed                                                  ErrorCode = 90000
+	ErrChannelOperationFailedInvalidChannelState                               ErrorCode = 90001
+	ErrChannelOperationFailedEpochExpiredOrNeverExisted                        ErrorCode = 90002
+	ErrUnableToRecoverChannelMessagesExpired                                   ErrorCode = 90003
+	ErrUnableToRecoverChannelMessageLimitExceeded                              ErrorCode = 90004
+	ErrUnableToRecoverChannelNoMatchingEpoch                                   ErrorCode = 90005
+	ErrUnableToRecoverChannelUnboundedRequest                                  ErrorCode = 90006
+	ErrChannelOperationFailedNoResponseFromServer                              ErrorCode = 90007
+	ErrMaximumNumberOfChannelsPerConnectionExceeded                            ErrorCode = 90010
+	ErrUnableToEnterPresenceChannelNoClientID                                  ErrorCode = 91000
+	ErrUnableToEnterPresenceChannelInvalidChannelState                         ErrorCode = 91001
+	ErrUnableToLeavePresenceChannelThatIsNotEntered                            ErrorCode = 91002
+	ErrUnableToEnterPresenceChannelMaximumMemberLimitExceeded                  ErrorCode = 91003
+	ErrUnableToAutomaticallyReEnterPresenceChannel                             ErrorCode = 91004
+	ErrPresenceStateIsOutOfSync                                                ErrorCode = 91005
+	ErrMemberImplicitlyLeftPresenceChannelConnectionClosed                     ErrorCode = 91100
 )
 
-var errCodeText = map[int]string{
-	10000: "no error",
-	40000: "bad request",
-	40001: "invalid request body",
-	40002: "invalid parameter name",
-	40003: "invalid parameter value",
-	40004: "invalid header",
-	40005: "invalid credential",
-	40006: "invalid connection id",
-	40007: "invalid message id",
-	40008: "invalid content length",
-	40009: "maximum message length exceeded",
-	40010: "invalid channel name",
-	40011: "stale ring state",
-	40012: "invalid client id",
-	40013: "Invalid message data or encoding",
-	40014: "Resource disposed",
-	40015: "Invalid device id",
-	40020: "Batch error",
-	40030: "Invalid publish request (unspecified)",
-	40031: "Invalid publish request (invalid client-specified id)",
-	40100: "unauthorized",
-	40101: "invalid credentials",
-	40102: "incompatible credentials",
-	40103: "invalid use of Basic auth over non-TLS transport",
-	40104: "timestamp not current",
-	40105: "nonce value replayed",
-	40106: "Unable to obtain credentials from given parameters",
-	40110: "account disabled",
-	40111: "account restricted (connection limits exceeded)",
-	40112: "account blocked (message limits exceeded)",
-	40113: "account blocked",
-	40114: "account restricted (channel limits exceeded)",
-	40120: "application disabled",
-	40130: "key error (unspecified)",
-	40131: "key revoked",
-	40132: "key expired",
-	40133: "key disabled",
-	40140: "token error (unspecified)",
-	40141: "token revoked",
-	40142: "token expired",
-	40143: "token unrecognised",
-	40144: "invalid JWT format",
-	40145: "invalid token format",
-	40150: "connection blocked (limits exceeded)",
-	40160: "operation not permitted with provided capability",
-	40170: "error from client token callback",
-	40300: "forbidden",
-	40310: "account does not permit tls connection",
-	40311: "operation requires tls connection",
-	40320: "application requires authentication",
-	40400: "not found",
-	40500: "method not allowed",
-	42910: "rate limit exceeded (nonfatal): request rejected (unspecified)",
-	42911: "max per-connection publish rate limit exceeded (nonfatal): unable to publish message",
-	42920: "rate limit exceeded (fatal)",
-	42921: "max per-connection publish rate limit exceeded (fatal); closing connection",
-	50000: "internal error",
-	50001: "internal channel error",
-	50002: "internal connection error",
-	50003: "timeout error",
-	50004: "Request failed due to overloaded instance",
-	70000: "reactor operation failed",
-	70001: "reactor operation failed (post operation failed)",
-	70002: "reactor operation failed (post operation returned unexpected code)",
-	70003: "reactor operation failed (maximum number of concurrent in-flight requests exceeded)",
-	71000: "Exchange error (unspecified)",
-	71001: "Forced re-attachment due to permissions change",
-	71100: "Exchange publisher error (unspecified)",
-	71101: "No such publisher",
-	71102: "Publisher not enabled as an exchange publisher",
-	71200: "Exchange product error (unspecified)",
-	71201: "No such product",
-	71202: "Product disabled",
-	71203: "No such channel in this product",
-	71300: "Exchange subscription error (unspecified)",
-	71301: "Subscription disabled",
-	71302: "Requester has no subscription to this product",
-	80000: "connection failed",
-	80001: "connection failed (no compatible transport)",
-	80002: "connection suspended",
-	80003: "disconnected",
-	80004: "already connected",
-	80005: "invalid connection id (remote not found)",
-	80006: "unable to recover connection (messages expired)",
-	80007: "unable to recover connection (message limit exceeded)",
-	80008: "unable to recover connection (connection expired)",
-	80009: "connection not established (no transport handle)",
-	80010: "invalid operation (invalid transport handle)",
-	80011: "unable to recover connection (incompatible auth params)",
-	80012: "unable to recover connection (invalid or unspecified connection serial)",
-	80013: "protocol error",
-	80014: "connection timed out",
-	80015: "incompatible connection parameters",
-	80016: "operation on superseded transport",
-	80017: "connection closed",
-	80018: "invalid connection id (invalid format)",
-	80019: "client configured authentication provider request failed",
-	80020: "continuity loss due to maximum subscribe message rate exceeded",
-	80030: "client restriction not satisfied",
-	90000: "channel operation failed",
-	90001: "channel operation failed (invalid channel state)",
-	90002: "channel operation failed (epoch expired or never existed)",
-	90003: "unable to recover channel (messages expired)",
-	90004: "unable to recover channel (message limit exceeded)",
-	90005: "unable to recover channel (no matching epoch)",
-	90006: "unable to recover channel (unbounded request)",
-	90007: "channel operation failed (no response from server)",
-	90010: "maximum number of channels per connection exceeded",
-	91000: "unable to enter presence channel (no clientId)",
-	91001: "unable to enter presence channel (invalid channel state)",
-	91002: "unable to leave presence channel that is not entered",
-	91003: "unable to enter presence channel (maximum member limit exceeded)",
-	91004: "unable to automatically re-enter presence channel",
-	91005: "presence state is out of sync",
-	91100: "member implicitly left presence channel (connection closed)",
+func (c ErrorCode) String() string {
+	switch c {
+	case ErrNotSet:
+		return "(error code not set)"
+	case ErrNoError:
+		return "no error"
+	case ErrBadRequest:
+		return "bad request"
+	case ErrInvalidRequestBody:
+		return "invalid request body"
+	case ErrInvalidParameterName:
+		return "invalid parameter name"
+	case ErrInvalidParameterValue:
+		return "invalid parameter value"
+	case ErrInvalidHeader:
+		return "invalid header"
+	case ErrInvalidCredential:
+		return "invalid credential"
+	case ErrInvalidConnectionID:
+		return "invalid connection id"
+	case ErrInvalidMessageID:
+		return "invalid message id"
+	case ErrInvalidContentLength:
+		return "invalid content length"
+	case ErrMaximumMessageLengthExceeded:
+		return "maximum message length exceeded"
+	case ErrInvalidChannelName:
+		return "invalid channel name"
+	case ErrStaleRingState:
+		return "stale ring state"
+	case ErrInvalidClientID:
+		return "invalid client id"
+	case ErrInvalidMessageDataOrEncoding:
+		return "Invalid message data or encoding"
+	case ErrResourceDisposed:
+		return "Resource disposed"
+	case ErrInvalidDeviceID:
+		return "Invalid device id"
+	case ErrBatchError:
+		return "Batch error"
+	case ErrInvalidPublishRequestUnspecified:
+		return "Invalid publish request (unspecified)"
+	case ErrInvalidPublishRequestInvalidClientSpecifiedID:
+		return "Invalid publish request (invalid client-specified id)"
+	case ErrUnauthorized:
+		return "unauthorized"
+	case ErrInvalidCredentials:
+		return "invalid credentials"
+	case ErrIncompatibleCredentials:
+		return "incompatible credentials"
+	case ErrInvalidUseOfBasicAuthOverNonTLSTransport:
+		return "invalid use of Basic auth over non-TLS transport"
+	case ErrTimestampNotCurrent:
+		return "timestamp not current"
+	case ErrNonceValueReplayed:
+		return "nonce value replayed"
+	case ErrUnableToObtainCredentialsFromGivenParameters:
+		return "Unable to obtain credentials from given parameters"
+	case ErrAccountDisabled:
+		return "account disabled"
+	case ErrAccountRestrictedConnectionLimitsExceeded:
+		return "account restricted (connection limits exceeded)"
+	case ErrAccountBlockedMessageLimitsExceeded:
+		return "account blocked (message limits exceeded)"
+	case ErrAccountBlocked:
+		return "account blocked"
+	case ErrAccountRestrictedChannelLimitsExceeded:
+		return "account restricted (channel limits exceeded)"
+	case ErrApplicationDisabled:
+		return "application disabled"
+	case ErrKeyErrorUnspecified:
+		return "key error (unspecified)"
+	case ErrKeyRevoked:
+		return "key revoked"
+	case ErrKeyExpired:
+		return "key expired"
+	case ErrKeyDisabled:
+		return "key disabled"
+	case ErrTokenErrorUnspecified:
+		return "token error (unspecified)"
+	case ErrTokenRevoked:
+		return "token revoked"
+	case ErrTokenExpired:
+		return "token expired"
+	case ErrTokenUnrecognised:
+		return "token unrecognised"
+	case ErrInvalidJWTFormat:
+		return "invalid JWT format"
+	case ErrInvalidTokenFormat:
+		return "invalid token format"
+	case ErrConnectionBlockedLimitsExceeded:
+		return "connection blocked (limits exceeded)"
+	case ErrOperationNotPermittedWithProvidedCapability:
+		return "operation not permitted with provided capability"
+	case ErrErrorFromClientTokenCallback:
+		return "error from client token callback"
+	case ErrForbidden:
+		return "forbidden"
+	case ErrAccountDoesNotPermitTLSConnection:
+		return "account does not permit tls connection"
+	case ErrOperationRequiresTLSConnection:
+		return "operation requires tls connection"
+	case ErrApplicationRequiresAuthentication:
+		return "application requires authentication"
+	case ErrNotFound:
+		return "not found"
+	case ErrMethodNotAllowed:
+		return "method not allowed"
+	case ErrRateLimitExceededNonfatal:
+		return "rate limit exceeded (nonfatal): request rejected (unspecified)"
+	case ErrMaxPerConnectionPublishRateLimitExceededNonfatal:
+		return "max per-connection publish rate limit exceeded (nonfatal): unable to publish message"
+	case ErrRateLimitExceededFatal:
+		return "rate limit exceeded (fatal)"
+	case ErrMaxPerConnectionPublishRateLimitExceededFatal:
+		return "max per-connection publish rate limit exceeded (fatal); closing connection"
+	case ErrInternalError:
+		return "internal error"
+	case ErrInternalChannelError:
+		return "internal channel error"
+	case ErrInternalConnectionError:
+		return "internal connection error"
+	case ErrTimeoutError:
+		return "timeout error"
+	case ErrRequestFailedDueToOverloadedInstance:
+		return "Request failed due to overloaded instance"
+	case ErrReactorOperationFailed:
+		return "reactor operation failed"
+	case ErrReactorOperationFailedPostOperationFailed:
+		return "reactor operation failed (post operation failed)"
+	case ErrReactorOperationFailedPostOperationReturnedUnexpectedCode:
+		return "reactor operation failed (post operation returned unexpected code)"
+	case ErrReactorOperationFailedMaximumNumberOfConcurrentInFlightRequestsExceeded:
+		return "reactor operation failed (maximum number of concurrent in-flight requests exceeded)"
+	case ErrExchangeErrorUnspecified:
+		return "Exchange error (unspecified)"
+	case ErrForcedReAttachmentDueToPermissionsChange:
+		return "Forced re-attachment due to permissions change"
+	case ErrExchangePublisherErrorUnspecified:
+		return "Exchange publisher error (unspecified)"
+	case ErrNoSuchPublisher:
+		return "No such publisher"
+	case ErrPublisherNotEnabledAsAnExchangePublisher:
+		return "Publisher not enabled as an exchange publisher"
+	case ErrExchangeProductErrorUnspecified:
+		return "Exchange product error (unspecified)"
+	case ErrNoSuchProduct:
+		return "No such product"
+	case ErrProductDisabled:
+		return "Product disabled"
+	case ErrNoSuchChannelInThisProduct:
+		return "No such channel in this product"
+	case ErrExchangeSubscriptionErrorUnspecified:
+		return "Exchange subscription error (unspecified)"
+	case ErrSubscriptionDisabled:
+		return "Subscription disabled"
+	case ErrRequesterHasNoSubscriptionToThisProduct:
+		return "Requester has no subscription to this product"
+	case ErrConnectionFailed:
+		return "connection failed"
+	case ErrConnectionFailedNoCompatibleTransport:
+		return "connection failed (no compatible transport)"
+	case ErrConnectionSuspended:
+		return "connection suspended"
+	case ErrDisconnected:
+		return "disconnected"
+	case ErrAlreadyConnected:
+		return "already connected"
+	case ErrInvalidConnectionIDRemoteNotFound:
+		return "invalid connection id (remote not found)"
+	case ErrUnableToRecoverConnectionMessagesExpired:
+		return "unable to recover connection (messages expired)"
+	case ErrUnableToRecoverConnectionMessageLimitExceeded:
+		return "unable to recover connection (message limit exceeded)"
+	case ErrUnableToRecoverConnectionConnectionExpired:
+		return "unable to recover connection (connection expired)"
+	case ErrConnectionNotEstablishedNoTransportHandle:
+		return "connection not established (no transport handle)"
+	case ErrInvalidOperationInvalidTransportHandle:
+		return "invalid operation (invalid transport handle)"
+	case ErrUnableToRecoverConnectionIncompatibleAuthParams:
+		return "unable to recover connection (incompatible auth params)"
+	case ErrUnableToRecoverConnectionInvalidOrUnspecifiedConnectionSerial:
+		return "unable to recover connection (invalid or unspecified connection serial)"
+	case ErrProtocolError:
+		return "protocol error"
+	case ErrConnectionTimedOut:
+		return "connection timed out"
+	case ErrIncompatibleConnectionParameters:
+		return "incompatible connection parameters"
+	case ErrOperationOnSupersededTransport:
+		return "operation on superseded transport"
+	case ErrConnectionClosed:
+		return "connection closed"
+	case ErrInvalidConnectionIDInvalidFormat:
+		return "invalid connection id (invalid format)"
+	case ErrClientConfiguredAuthenticationProviderRequestFailed:
+		return "client configured authentication provider request failed"
+	case ErrContinuityLossDueToMaximumSubscribeMessageRateExceeded:
+		return "continuity loss due to maximum subscribe message rate exceeded"
+	case ErrClientRestrictionNotSatisfied:
+		return "client restriction not satisfied"
+	case ErrChannelOperationFailed:
+		return "channel operation failed"
+	case ErrChannelOperationFailedInvalidChannelState:
+		return "channel operation failed (invalid channel state)"
+	case ErrChannelOperationFailedEpochExpiredOrNeverExisted:
+		return "channel operation failed (epoch expired or never existed)"
+	case ErrUnableToRecoverChannelMessagesExpired:
+		return "unable to recover channel (messages expired)"
+	case ErrUnableToRecoverChannelMessageLimitExceeded:
+		return "unable to recover channel (message limit exceeded)"
+	case ErrUnableToRecoverChannelNoMatchingEpoch:
+		return "unable to recover channel (no matching epoch)"
+	case ErrUnableToRecoverChannelUnboundedRequest:
+		return "unable to recover channel (unbounded request)"
+	case ErrChannelOperationFailedNoResponseFromServer:
+		return "channel operation failed (no response from server)"
+	case ErrMaximumNumberOfChannelsPerConnectionExceeded:
+		return "maximum number of channels per connection exceeded"
+	case ErrUnableToEnterPresenceChannelNoClientID:
+		return "unable to enter presence channel (no clientId)"
+	case ErrUnableToEnterPresenceChannelInvalidChannelState:
+		return "unable to enter presence channel (invalid channel state)"
+	case ErrUnableToLeavePresenceChannelThatIsNotEntered:
+		return "unable to leave presence channel that is not entered"
+	case ErrUnableToEnterPresenceChannelMaximumMemberLimitExceeded:
+		return "unable to enter presence channel (maximum member limit exceeded)"
+	case ErrUnableToAutomaticallyReEnterPresenceChannel:
+		return "unable to automatically re-enter presence channel"
+	case ErrPresenceStateIsOutOfSync:
+		return "presence state is out of sync"
+	case ErrMemberImplicitlyLeftPresenceChannelConnectionClosed:
+		return "member implicitly left presence channel (connection closed)"
+	}
+	return ""
 }
diff --git a/ably/generate.go b/ably/generate.go
new file mode 100644
index 0000000..b239791
--- /dev/null
+++ b/ably/generate.go
@@ -0,0 +1,3 @@
+package ably
+
+//go:generate go run ../scripts/errors.go -json ../common/protocol/errors.json -o errors.go
diff --git a/ably/http_paginated_response.go b/ably/http_paginated_response.go
index 662d969..47277d1 100644
--- a/ably/http_paginated_response.go
+++ b/ably/http_paginated_response.go
@@ -12,7 +12,7 @@ type HTTPPaginatedResponse struct {
 	*PaginatedResult
 	StatusCode   int         //spec HP4
 	Success      bool        //spec HP5
-	ErrorCode    int         //spec HP6
+	ErrorCode    ErrorCode   //spec HP6
 	ErrorMessage string      //spec HP7
 	Headers      http.Header //spec HP8
 }
@@ -42,7 +42,7 @@ func newHTTPPaginatedResultFromPaginatedResult(p *PaginatedResult) *HTTPPaginate
 	h := &HTTPPaginatedResponse{PaginatedResult: p}
 	h.StatusCode = p.statusCode
 	h.Success = p.success
-	h.ErrorCode = p.errorCode
+	h.ErrorCode = ErrorCode(p.errorCode)
 	h.ErrorMessage = p.errorMessage
 	return h
 }
diff --git a/ably/realtime_channel.go b/ably/realtime_channel.go
index 6a14c3d..c4e91d7 100644
--- a/ably/realtime_channel.go
+++ b/ably/realtime_channel.go
@@ -460,7 +460,7 @@ func (c *RealtimeChannel) notify(msg *proto.ProtocolMessage) {
 	case proto.ActionDetached:
 		c.mtx.Lock()
 
-		err := error(newErrorProto(msg.Error))
+		err := error(newErrorFromProto(msg.Error))
 		switch c.state {
 		case ChannelStateDetaching:
 			c.lockSetState(ChannelStateDetached, err)
@@ -501,8 +501,8 @@ func (c *RealtimeChannel) notify(msg *proto.ProtocolMessage) {
 	case proto.ActionPresence:
 		c.Presence.processIncomingMessage(msg, "")
 	case proto.ActionError:
-		c.setState(ChannelStateFailed, newErrorProto(msg.Error))
-		c.queue.Fail(newErrorProto(msg.Error))
+		c.setState(ChannelStateFailed, newErrorFromProto(msg.Error))
+		c.queue.Fail(newErrorFromProto(msg.Error))
 	case proto.ActionMessage:
 		c.subs.messageEnqueue(msg)
 	default:
diff --git a/ably/realtime_client.go b/ably/realtime_client.go
index f30cdc2..19864f9 100644
--- a/ably/realtime_client.go
+++ b/ably/realtime_client.go
@@ -85,7 +85,7 @@ func (c *Realtime) onReconnected(err *proto.ErrorInfo, isNewID bool) {
 
 func (c *Realtime) onReconnectionFailed(err *proto.ErrorInfo) {
 	for _, ch := range c.Channels.All() {
-		ch.setState(ChannelStateFailed, newErrorProto(err))
+		ch.setState(ChannelStateFailed, newErrorFromProto(err))
 	}
 }
 
diff --git a/ably/realtime_conn.go b/ably/realtime_conn.go
index 5b563e6..d1f6971 100644
--- a/ably/realtime_conn.go
+++ b/ably/realtime_conn.go
@@ -524,12 +524,12 @@ func (c *Connection) eventloop() {
 		case proto.ActionHeartbeat:
 		case proto.ActionAck:
 			c.mtx.Lock()
-			c.pending.Ack(msg.MsgSerial, msg.Count, newErrorProto(msg.Error))
+			c.pending.Ack(msg.MsgSerial, msg.Count, newErrorFromProto(msg.Error))
 			c.setSerial(c.serial + 1)
 			c.mtx.Unlock()
 		case proto.ActionNack:
 			c.mtx.Lock()
-			c.pending.Nack(msg.MsgSerial, msg.Count, newErrorProto(msg.Error))
+			c.pending.Nack(msg.MsgSerial, msg.Count, newErrorFromProto(msg.Error))
 			c.mtx.Unlock()
 		case proto.ActionError:
 			if msg.Channel != "" {
@@ -542,7 +542,7 @@ func (c *Connection) eventloop() {
 			c.reauthorizing = false
 			if isTokenError(msg.Error) {
 				if reauthorizing {
-					c.lockedReauthorizationFailed(newErrorProto(msg.Error))
+					c.lockedReauthorizationFailed(newErrorFromProto(msg.Error))
 					c.mtx.Unlock()
 					return
 				} else {
@@ -586,7 +586,7 @@ func (c *Connection) eventloop() {
 			if reconnecting {
 				// (RTN15c1) (RTN15c2)
 				c.mtx.Lock()
-				c.lockSetState(ConnectionStateConnected, newErrorProto(msg.Error))
+				c.lockSetState(ConnectionStateConnected, newErrorFromProto(msg.Error))
 				c.mtx.Unlock()
 				if previousID != msg.ConnectionID {
 					// (RTN15c3)
@@ -640,9 +640,9 @@ func (c *Connection) failedConnSideEffects(err *proto.ErrorInfo) {
 		c.reauthorizing = false
 		c.callbacks.onReconnectionFailed(err)
 	}
-	c.lockSetState(ConnectionStateFailed, newErrorProto(err))
+	c.lockSetState(ConnectionStateFailed, newErrorFromProto(err))
 	c.mtx.Unlock()
-	c.queue.Fail(newErrorProto(err))
+	c.queue.Fail(newErrorFromProto(err))
 	if c.conn != nil {
 		c.conn.Close()
 	}
diff --git a/ably/rest_client.go b/ably/rest_client.go
index 89de5d6..d9eee65 100644
--- a/ably/rest_client.go
+++ b/ably/rest_client.go
@@ -218,9 +218,9 @@ func (c *REST) Request(method string, path string, params *PaginateParams, body
 			})
 		}, c.logger())
 	default:
-		return nil, newErrorProto(&proto.ErrorInfo{
+		return nil, newErrorFromProto(&proto.ErrorInfo{
 			Message:    fmt.Sprintf("%s method is not supported", method),
-			Code:       ErrMethodNotAllowed,
+			Code:       int(ErrMethodNotAllowed),
 			StatusCode: http.StatusMethodNotAllowed,
 		})
 	}
diff --git a/scripts/errors.go b/scripts/errors.go
index 947e816..d2722bf 100644
--- a/scripts/errors.go
+++ b/scripts/errors.go
@@ -19,7 +19,7 @@ type options struct {
 	output     string
 }
 
-const version = "0.1.0"
+const version = "0.2.0"
 
 func main() {
 	log.SetFlags(log.Llongfile)
@@ -118,15 +118,26 @@ const errorsTemplate = `// Code generated by errors-const v{{.version}} tool DO
 
 package ably
 
-// ably error codes
+// ErrorCode is the type for predefined Ably error codes.
+type ErrorCode int
+
 const(
+	ErrNotSet ErrorCode = 0
 	{{- range .codes}}
-	Err{{key .|normalize|split " "|map "id" "ID"|map "clientId" "clientID"|map "tls" "TLS"|title|join ""}} = {{.}}
+	{{template "constName" .}} ErrorCode = {{.}}
 	{{- end}}
 )
-var errCodeText=map[int]string{
+
+func (c ErrorCode) String() string {
+	switch c {
+	case ErrNotSet:
+		return "(error code not set)"
 	{{- range .codes}}
-	{{.}}:"{{key .}}",
+	case {{template "constName" .}}:
+		return "{{key .}}"
 	{{- end}}
+	}
+	return ""
 }
+{{- define "constName"}}Err{{key .|normalize|split " "|map "id" "ID"|map "clientId" "clientID"|map "tls" "TLS"|title|join ""}}{{- end}}
 `


Test patch:
diff --git a/ably/ably_test.go b/ably/ably_test.go
index a1672f5..e6ecc02 100644
--- a/ably/ably_test.go
+++ b/ably/ably_test.go
@@ -56,7 +56,7 @@ func safeclose(t *testing.T, closers ...io.Closer) {
 	}
 }
 
-func checkError(code int, err error) error {
+func checkError(code ably.ErrorCode, err error) error {
 	switch e, ok := err.(*ably.ErrorInfo); {
 	case !ok:
 		return fmt.Errorf("want err to be *ably.ErrorInfo; was %T: %v", err, err)
diff --git a/ably/auth_test.go b/ably/auth_test.go
index b658f7c..a94187f 100644
--- a/ably/auth_test.go
+++ b/ably/auth_test.go
@@ -78,8 +78,8 @@ func TestAuth_BasicAuth(t *testing.T) {
 	switch _, err := ably.NewREST(app.Options(opts.TLS(false))); {
 	case err == nil:
 		t.Fatal("want err != nil")
-	case ably.ErrorCode(err) != 40103:
-		t.Fatalf("want code=40103; got %d", ably.ErrorCode(err))
+	case ably.UnwrapErrorCode(err) != 40103:
+		t.Fatalf("want code=40103; got %d", ably.UnwrapErrorCode(err))
 	}
 }
 
diff --git a/ably/error_test.go b/ably/error_test.go
index 7841279..a799994 100644
--- a/ably/error_test.go
+++ b/ably/error_test.go
@@ -23,7 +23,7 @@ func TestErrorResponseWithInvalidKey(t *testing.T) {
 	err, ok := e.(*ably.ErrorInfo)
 	assert.True(t, ok, fmt.Sprintf("want e be *ably.Error; was %T", e))
 	assert.Equal(t, 400, err.StatusCode, fmt.Sprintf("want StatusCode=400; got %d", err.StatusCode))
-	assert.Equal(t, 40005, err.Code, fmt.Sprintf("want Code=40005; got %d", err.Code))
+	assert.Equal(t, ably.ErrInvalidCredential, err.Code, fmt.Sprintf("want Code=	; got %d", err.Code))
 	assert.NotNil(t, err.Unwrap())
 }
 
diff --git a/ably/export_test.go b/ably/export_test.go
index 189fbba..f5456f6 100644
--- a/ably/export_test.go
+++ b/ably/export_test.go
@@ -36,7 +36,7 @@ func DecodeResp(resp *http.Response, out interface{}) error {
 	return decodeResp(resp, out)
 }
 
-func ErrorCode(err error) int {
+func UnwrapErrorCode(err error) ErrorCode {
 	return code(err)
 }
 
@@ -70,7 +70,7 @@ func (opts *clientOptions) GetFallbackRetryTimeout() time.Duration {
 	return opts.fallbackRetryTimeout()
 }
 
-func NewErrorInfo(code int, err error) *ErrorInfo {
+func NewErrorInfo(code ErrorCode, err error) *ErrorInfo {
 	return newError(code, err)
 }
 
diff --git a/ably/realtime_conn_spec_test.go b/ably/realtime_conn_spec_test.go
index 7ad6766..a47d2b5 100644
--- a/ably/realtime_conn_spec_test.go
+++ b/ably/realtime_conn_spec_test.go
@@ -1298,7 +1298,7 @@ func TestRealtimeConn_RTN15h1_OnDisconnectedCannotRenewToken(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	if errInfo.StatusCode != tokenErr.StatusCode || errInfo.Code != tokenErr.Code {
+	if errInfo.StatusCode != tokenErr.StatusCode || int(errInfo.Code) != tokenErr.Code {
 		t.Fatalf("expected the token error as FAILED state change reason; got: %s", err)
 	}
 }
@@ -1436,7 +1436,7 @@ func TestRealtimeConn_RTN15h2_ReauthWithBadToken(t *testing.T) {
 		t.Fatalf("expected DISCONNECTED event; got %v", change)
 	}
 
-	if change.Reason.StatusCode != tokenErr.StatusCode || change.Reason.Code != tokenErr.Code {
+	if change.Reason.StatusCode != tokenErr.StatusCode || int(change.Reason.Code) != tokenErr.Code {
 		t.Fatalf("expected the token error as FAILED state change reason; got: %s", change.Reason)
 	}
 }
@@ -1584,7 +1584,7 @@ func TestRealtimeConn_RTN15i_OnErrorWhenConnected(t *testing.T) {
 	if !errors.As(err, &errorInfo) {
 		t.Fatal(err)
 	}
-	if expected, got := errorCode, errorInfo.Code; expected != got {
+	if expected, got := errorCode, int(errorInfo.Code); expected != got {
 		t.Fatalf("expected error code %d; got %v", expected, errorInfo)
 	}
 
@@ -1689,12 +1689,12 @@ func TestRealtimeConn_RTN16(t *testing.T) {
 		{ //(RTN16e)
 			info := err.(*ably.ErrorInfo)
 			code := 80008
-			if info.Code != code {
+			if int(info.Code) != code {
 				// verify unrecoverable-connection error set in stateChange.reason
 				t.Errorf("expected 80000 got %d", info.Code)
 			}
 			reason := client2.Connection.ErrorReason()
-			if reason.Code != code {
+			if int(reason.Code) != code {
 				// verify unrecoverable-connection error set in connection.errorReason
 				t.Errorf("expected 80000 got %d", reason.Code)
 			}