Repository: nats-io/nats-server
Pull request #2168 (2021-04-30 13:10:22)

richer api errors proposal
This creates richer errors in response to https://github.com/nats-io/nats-server/issues/1811 and numerous other requests.

The main idea is that when a 404 is raised in response to accessing a consumer we now include a unique identifier that would allow callers to distinguish between stream not found, consumer not found, no message found and other errors that might raise 404.

One could argue that users might parse the Description like `stream not  found` however this makes our human friendly descriptions part of the API promises and we can never improve, bug fix or translate those strings. For example today there is an unfortunate mix of `jetstream` and `JetStream` in use in error descriptions and we would want the ability to improve errors without breaking peoples code.

Additionally I propose capturing help and urls in the code - not in actual API responses - to facilitate a single place where we maintain these errors and then adding tooling like `nats error JS1` that will produce the error text, help, additional information and links to documentation that would help users resolve those errors.  So the slow `ApiErrorByKind` would not be used in anything but the CLI to resolve these things, though even thats optional as the `ApiErrors` is exported.

Signed-off-by: R.I.Pienaar <rip@devco.net>

/cc @nats-io/core


ripienaar: @derekcollison I want to spend some time on this during this week, let me know what you think of the approach before I dive in on a wrong track, thanks!
derekcollison: Probably deserves a meeting between a few folks.
ripienaar: I added @matthiashanel and @aricart to reviewers for context, will have a call with all reviewers I guess.
derekcollison: Add me onto the list as well if you can.
aricart: I had looked at the PR before, and I agree that having a code that can be pointed at documentation can provide a richer experience to diagnose problems. The standard string that it represents is useful, but more so I think is the case where these values make it to a server log. Having documentation where the lookup can be performed is very useful. 
ripienaar: @kozlovic think I am good on this one, I updated MQTT code considering new errors and also added a ADR for how it works and how to maintain it (though I think that needs expanding in time).

for now keen to get this in so we can stop the rebasing pain :)
ripienaar: While we wait for @derekcollison to give this a look the one thing I've been mulling over is how we format the errors that are of the ErrorF form.

```
ApiErrors[JSStreamExternalApiOverlapErrF].ErrOrNewF(foo, bar)
```

Where the ApiErr is:

```
{Code: 400, ErrCode: 10021, Description: "stream external api prefix %q must not overlap with %s"},
```

The  problem here is that we want to be able to extend and change these errors - especially if we later need to translate them - then the order based replace becomes an error.

The typical fix is to make the description something like `"stream external api prefix {prefix} must not overlap with {api}"` and then:

```
ApiErrors[JSStreamExternalApiOverlapErrF].ErrOrNewF("{prefix}", foo, "{api}", bar)
```

This is much more robust and future proof, personally I think it's worth it, but curious what @ColinSullivan1 (who like me also seem to care for translation) and @kozlovic thinks
kozlovic: > This is much more robust and future proof, personally I think it's worth it, but curious what @ColinSullivan1 (who like me also seem to care for translation) and @kozlovic thinks

I agree.
ripienaar: I squashed this but I think I need more time to find a better solution to handle the named token replace things, forcing all things to be strings is terrible which is what strings.Replacer() wants.  So will investigate more.
derekcollison: We could do the string replace solution at a different time as well.

Patch:
diff --git a/doc/README.md b/doc/README.md
index e65e7b83..87e45cdc 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -18,4 +18,4 @@ good starting point. We do not have a fixed format at present.
 
 ## ADR Statuses
 
-Each ADR has a status, let's try to use `Proposed`, `Approved` and `Rejected`.
+Each ADR has a status, let's try to use `Proposed`, `Approved` `Partially Implemented`, `Implemented` and `Rejected`.
diff --git a/doc/adr/0001-jetstream-json-api-design.md b/doc/adr/0001-jetstream-json-api-design.md
index 458420b3..1a88e828 100644
--- a/doc/adr/0001-jetstream-json-api-design.md
+++ b/doc/adr/0001-jetstream-json-api-design.md
@@ -7,25 +7,10 @@ Author: @ripienaar
 
 Partially Implemented
 
-The JSON API has been updated, however the urlencoded replies have not been adopted.
-
 ## Context
 
 At present, the API encoding consists of mixed text and JSON, we should improve consistency and error handling.
 
-## Decision
-
-We anticipate many types of consumer for JetStream including very small devices via MQTT and those written in 
-languages without easy to use JSON support like C.
-
-We will make it easy for light-weight clients by using `+OK` and `-ERR` style prefixes for client interactions,
-but the server management API will be JSON all the time. The rationale being that such small devices will most
-likely have their Consumers and Streams provisioned as part of the platform and often might not care for the
-additional context.
-
-In client responses we will use URL Encoding to provide additional context so multiple fields worth of
-context can be provided. URL Encoding has [broad language support](https://www.rosettacode.org/wiki/URL_decoding).
-
 ### Admin APIs
 
 #### Requests
@@ -72,14 +57,15 @@ Successful Stream Info:
 }
 ```
 
-Stream Info Error:
+Consumer Info Error:
 
 ```json
 {
-  "type": "io.nats.jetstream.api.v1.stream_info",
+  "type": "io.nats.jetstream.api.v1.consumer_info",
   "error": {
-    "description": "unknown stream bob",
-    "code": 400
+    "description": "consumer not found",
+    "code": 404,
+    "error_code": 10059
   }
 }
 ```
@@ -88,7 +74,12 @@ Here we have a minimally correct response with the additional error object.
 
 In the `error` struct we have `description` as a short human friendly explanation that should include enough context to
 identify what Stream or Consumer acted on and whatever else we feel will help the user while not sharing privileged account
-information.  `code` will be HTTP like, 400 human error, 500 server error etc.
+information.  These strings are not part of the API promises, we can update and re-word or translate them at any time. Programmatic
+error handling should look at the `code` which will be HTTP like, 4xx human error, 5xx server error etc. Finally, the `error_code`
+indicates the specific reason for the 404 - here `10059` means the stream did not exist, helping developers identify the
+real underlying cause. 
+
+More information about the `error_code` system can be found in [ADR-7](0007-error-codes.md).
 
 Ideally the error response includes a minimally valid body of what was requested but this can be very hard to implement correctly.
 
@@ -106,17 +97,6 @@ Today the list API's just return `["ORDERS"]`, these will become:
 
 With the same `error` treatment when some error happens.
 
-### Client Interactions
-
-To keep clients on small devices viable we will standardise our responses as in the examples below. 
-
- * `+OK` everything is fine no additional context 
- * `+OK "stream=ORDERS&sequence=10"` everything is fine, we have additional data to pass using standard URL encoding
- * `-ERR` something failed, we have no reason
- * `-ERR "reason=reason%20for%20failure&stream=STREAM"` something failed, we have additional context to provide using standard URL encoding
-
-Additional information will be needed for example when there are overlapping Streams and one of them fail. 
-
 ## Implementation
 
 While implementing this in JetStream the following pattern emerged:
@@ -128,8 +108,11 @@ type JSApiResponse struct {
 }
 
 type ApiError struct {
-	Code        int    `json:"code"`
-	Description string `json:"description,omitempty"`
+    Code        int    `json:"code"`
+    ErrCode     int    `json:"err_code,omitempty"`
+    Description string `json:"description,omitempty"`
+    URL         string `json:"-"`
+    Help        string `json:"-"`
 }
 
 type JSApiConsumerCreateResponse struct {
@@ -169,4 +152,4 @@ Validating this in JSON Schema draft 7 is a bit awkward, not impossible and spec
 
 ## Consequences
 
-URL Encoding does not carry data types and the response fields will need documenting.
+URL Encoding does not carry data types, and the response fields will need documenting.
diff --git a/doc/adr/0007-error-codes.md b/doc/adr/0007-error-codes.md
new file mode 100644
index 00000000..e640c069
--- /dev/null
+++ b/doc/adr/0007-error-codes.md
@@ -0,0 +1,146 @@
+# 7. NATS Server Error Codes
+
+Date: 2021-05-12
+Author: @ripienaar
+
+## Status
+
+Partially Implemented in [#1811](https://github.com/nats-io/nats-server/issues/1811)
+
+The current focus is JetStream APIs, we will as a followup do a refactor and generalization and move onto other
+areas of the server.
+
+## Context
+
+When a developer performs a Consumer Info API request she might get a 404 error, there is no way to know if this is
+a 404 due to the Stream not existing or the Consumer not existing. The only way is to parse the returned error description
+like `consumer not found`. Further several other error situations might arise which due to our code design would be surfaced
+as a 404 when in fact it's more like a 5xx error - I/O errors and such.
+
+If users are parsing our error strings it means our error text form part of the Public API, we can never improve errors, 
+fix spelling errors or translate errors into other languages.
+
+This ADR describes an additional `error_code` that provides deeper context into the underlying cause of the 404.
+
+## Design
+
+We will adopt a numbering system for our errors where every error has a unique number within a range that indicates the 
+subsystem it belongs to.
+
+|Range|Description|
+|-----|-----------|
+|1xxxx|JetStream related errors|
+|2xxxx|MQTT related errors|
+
+The JetStream API error will be adjusted like this initially with later work turning this into a more generic error
+usable in other parts of the NATS Server code base.
+
+```go
+// ApiError is included in all responses if there was an error.
+type ApiError struct {
+	Code        int    `json:"code"`
+	ErrCode     int    `json:"err_code,omitempty"`
+	Description string `json:"description,omitempty"`
+	URL         string `json:"-"`
+	Help        string `json:"-"`
+}
+```
+
+Here the `code` and `error_code` is what we'll consider part of the Public API with `description` being specifically
+out of scope for SemVer protection and changes to these will not be considered a breaking change.
+
+The `ApiError` type will implement `error` and whenever it will be logged will append the code to the log line, for example:
+
+```
+stream not found (10059)
+```
+
+The `nats` CLI will have a lookup system like `nats error 10059` that will show details of this error including help,
+urls and such. It will also assist in listing and searching errors.  The same could be surfaced later in documentation
+and other areas.
+
+## Using in code
+
+### Raising an error
+
+Here we raise a `stream not found` error without providing any additional context to the user, the constant is 
+`JSStreamNotFoundErr` from which you can guess it takes no printf style interpolations vs one that does which would
+end in `...ErrF`:
+
+The go doc for this constant would also include the content of the error to assist via intellisense in your IDE.
+
+```go
+err = doThing()
+if err != nil {
+	return ApiErrors[JSStreamNotFoundErr]
+}
+```
+
+If we have to do string interpolation of the error body, here the `JSStreamRestoreErrF` has the body 
+`"restore failed: {err}"`, the `NewT()` will simply use `strings.Replaces()` to create a new `ApiError` with the full string,
+note this is a new instance of ApiError so normal compare of `err == ApiErrors[x]` won't match:
+
+```go
+err = doRestore()
+if err != nil {
+	return ApiErrors[JSStreamRestoreErrF].NewT("{err}", err)
+}
+```
+
+If we had to handle an error that may be an `ApiError` or a traditional go error we can use the `ErrOr` function, 
+this will look at the result from `lookupConsumer()`, if it's an `ApiError` that error will be set else `JSConsumerNotFoundErr` be 
+returned. Essentially the `lookupConsumer()` would return a `JSStreamNotFoundErr` if the stream does not exist else
+a `JSConsumerNotFoundErr` or go error on I/O failure for example.
+
+```go
+var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}}
+
+_, err = lookupConsumer(stream, consumer)
+if err != nil {
+    resp.Error = ApiErrors[JSConsumerNotFoundErr].ErrOr(err)
+}
+```
+
+While the `ErrOr` function returns the `ApiErrors` pointer exactly - meaning `err == ApiErrors[x]`, the counterpart
+`ErrOrNewT` will create a new instance.
+
+### Testing Errors
+
+Should you need to determine if a error is of a specific kind (error code) this can be done using the `IsNatsErr()` function:
+
+```go
+err = doThing()
+if IsNatsErr(err, JSStreamNotFoundErr, JSConsumerNotFoundErr) {
+	// create the stream and consumer
+} else if err !=nil{
+	// other critical failure
+}
+```
+
+## Maintaining the errors
+
+The file `errors.json` holds the data used to generate the error constants, lists etc
+
+```json
+[
+  {
+    "constant": "JSClusterPeerNotMemberErr",
+    "code": 400,
+    "error_code": 10040,
+    "description": "peer not a member"
+  },
+  {
+    "constant": "JSNotEnabledErr",
+    "code": 503,
+    "error_code": 10039,
+    "description": "JetStream not enabled for account",
+    "help": "This error indicates that JetStream is not enabled either at a global level or at global and account level",
+    "url": "https://docs.nats.io/jetstream"
+  }
+]
+```
+
+After editing this file run `go generate` in the top of the `nats-server` repo, and it will update the needed files.  Check
+in the result.
+
+When run this will verify that the `error_code` and `constant` is unique in each error
diff --git a/main.go b/main.go
index d1e005cf..f762b183 100644
--- a/main.go
+++ b/main.go
@@ -13,6 +13,8 @@
 
 package main
 
+//go:generate go run server/errors_gen.go
+
 import (
 	"flag"
 	"fmt"
diff --git a/server/errors.go b/server/errors.go
index ddad6423..f5db0c48 100644
--- a/server/errors.go
+++ b/server/errors.go
@@ -171,47 +171,11 @@ var (
 	ErrMalformedSubject = errors.New("malformed subject")
 
 	// ErrSubscribePermissionViolation is returned when processing of a subscription fails due to permissions.
-	ErrSubscribePermissionViolation = errors.New("subscribe permission viloation")
+	ErrSubscribePermissionViolation = errors.New("subscribe permission violation")
 
 	// ErrNoTransforms signals no subject transforms are available to map this subject.
 	ErrNoTransforms = errors.New("no matching transforms available")
 
-	// ErrJetStreamNotEnabled is returned when JetStream is not enabled.
-	ErrJetStreamNotEnabled = errors.New("jetstream not enabled")
-
-	// ErrJetStreamStreamNotFound is returned when a stream can not be found.
-	ErrJetStreamStreamNotFound = errors.New("stream not found")
-
-	// ErrJetStreamStreamAlreadyUsed is returned when a stream name has already been taken.
-	ErrJetStreamStreamAlreadyUsed = errors.New("stream name already in use")
-
-	// ErrJetStreamConsumerAlreadyUsed is returned when a consumer name has already been taken.
-	ErrJetStreamConsumerAlreadyUsed = errors.New("consumer name already in use")
-
-	// ErrJetStreamNotEnabledForAccount is returned JetStream is not enabled for this account.
-	ErrJetStreamNotEnabledForAccount = errors.New("jetstream not enabled for account")
-
-	// ErrJetStreamNotLeader is returned when issuing commands to a cluster on the wrong server.
-	ErrJetStreamNotLeader = errors.New("jetstream cluster can not handle request")
-
-	// ErrJetStreamNotAssigned is returned when the resource (stream or consumer) is not assigned.
-	ErrJetStreamNotAssigned = errors.New("jetstream cluster not assigned to this server")
-
-	// ErrJetStreamNotClustered is returned when a call requires clustering and we are not.
-	ErrJetStreamNotClustered = errors.New("jetstream not in clustered mode")
-
-	// ErrJetStreamResourcesExceeded is returned when a call would exceed internal resource limits.
-	ErrJetStreamResourcesExceeded = errors.New("jetstream resources exceeded for server")
-
-	// ErrStorageResourcesExceeded is returned when storage resources would be exceeded.
-	ErrStorageResourcesExceeded = errors.New("insufficient storage resources available")
-
-	// ErrMemoryResourcesExceeded is returned when memory resources would be exceeded.
-	ErrMemoryResourcesExceeded = errors.New("insufficient memory resources available")
-
-	// ErrReplicasNotSupported is returned when a stream with replicas > 1 in non-clustered mode.
-	ErrReplicasNotSupported = errors.New("replicas > 1 not supported in non-clustered mode")
-
 	// ErrCertNotPinned is returned when pinned certs are set and the certificate is not in it
 	ErrCertNotPinned = errors.New("certificate not pinned")
 )
@@ -297,7 +261,7 @@ func NewErrorCtx(err error, format string, args ...interface{}) error {
 	return &errCtx{err, fmt.Sprintf(format, args...)}
 }
 
-// implement to work with errors.Is and errors.As
+// Unwrap implement to work with errors.Is and errors.As
 func (e *errCtx) Unwrap() error {
 	if e == nil {
 		return nil
@@ -313,7 +277,7 @@ func (e *errCtx) Context() string {
 	return e.ctx
 }
 
-// Return Error or, if type is right error and context
+// UnpackIfErrorCtx return Error or, if type is right error and context
 func UnpackIfErrorCtx(err error) string {
 	if e, ok := err.(*errCtx); ok {
 		if _, ok := e.error.(*errCtx); ok {
@@ -336,7 +300,7 @@ func errorsUnwrap(err error) error {
 	return u.Unwrap()
 }
 
-// implements: go 1.13 errors.Is(err, target error) bool
+// ErrorIs implements: go 1.13 errors.Is(err, target error) bool
 // TODO replace with native code once we no longer support go1.12
 func ErrorIs(err, target error) bool {
 	// this is an outright copy of go 1.13 errors.Is(err, target error) bool
diff --git a/server/errors.json b/server/errors.json
new file mode 100644
index 00000000..8b8bedbd
--- /dev/null
+++ b/server/errors.json
@@ -0,0 +1,478 @@
+[
+  {
+    "constant": "JSClusterPeerNotMemberErr",
+    "code": 400,
+    "error_code": 10040,
+    "description": "peer not a member"
+  },
+  {
+    "constant": "JSConsumerEphemeralWithDurableInSubjectErr",
+    "code": 400,
+    "error_code": 10019,
+    "description": "consumer expected to be ephemeral but detected a durable name set in subject"
+  },
+  {
+    "constant": "JSStreamExternalDelPrefixOverlapsErrF",
+    "code": 400,
+    "error_code": 10022,
+    "description": "stream external delivery prefix {prefix} overlaps with stream subject {subject}"
+  },
+  {
+    "constant": "JSAccountResourcesExceededErr",
+    "code": 400,
+    "error_code": 10002,
+    "description": "resource limits exceeded for account"
+  },
+  {
+    "constant": "JSClusterNotAvailErr",
+    "code": 503,
+    "error_code": 10008,
+    "description": "JetStream system temporarily unavailable"
+  },
+  {
+    "constant": "JSStreamSubjectOverlapErr",
+    "code": 500,
+    "error_code": 10065,
+    "description": "subjects overlap with an existing stream"
+  },
+  {
+    "constant": "JSStreamWrongLastSequenceErrF",
+    "code": 400,
+    "error_code": 10071,
+    "description": "wrong last sequence: {seq}"
+  },
+  {
+    "constant": "JSTemplateNameNotMatchSubjectErr",
+    "code": 400,
+    "error_code": 10073,
+    "description": "template name in subject does not match request"
+  },
+  {
+    "constant": "JSClusterNoPeersErr",
+    "code": 400,
+    "error_code": 10005,
+    "description": "no suitable peers for placement"
+  },
+  {
+    "constant": "JSConsumerEphemeralWithDurableNameErr",
+    "code": 400,
+    "error_code": 10020,
+    "description": "consumer expected to be ephemeral but a durable name was set in request"
+  },
+  {
+    "constant": "JSInsufficientResourcesErr",
+    "code": 503,
+    "error_code": 10023,
+    "description": "insufficient resources",
+    "deprecates": "ErrJetStreamResourcesExceeded"
+  },
+  {
+    "constant": "JSMirrorMaxMessageSizeTooBigErr",
+    "code": 400,
+    "error_code": 10030,
+    "description": "stream mirror must have max message size >= source"
+  },
+  {
+    "constant": "JSStreamTemplateDeleteErrF",
+    "code": 500,
+    "error_code": 10067,
+    "description": "{err}",
+    "comment": "Generic stream template deletion failed error string"
+  },
+  {
+    "constant": "JSBadRequestErr",
+    "code": 400,
+    "error_code": 10003,
+    "description": "bad request"
+  },
+  {
+    "constant": "JSClusterUnSupportFeatureErr",
+    "code": 503,
+    "error_code": 10036,
+    "description": "not currently supported in clustered mode"
+  },
+  {
+    "constant": "JSConsumerNotFoundErr",
+    "code": 404,
+    "error_code": 10014,
+    "description": "consumer not found"
+  },
+  {
+    "constant": "JSSourceMaxMessageSizeTooBigErr",
+    "code": 400,
+    "error_code": 10046,
+    "description": "stream source must have max message size >= target"
+  },
+  {
+    "constant": "JSStreamAssignmentErrF",
+    "code": 500,
+    "error_code": 10048,
+    "description": "{err}",
+    "commend": "Generic stream assignment error string"
+  },
+  {
+    "constant": "JSStreamMessageExceedsMaximumErr",
+    "code": 400,
+    "error_code": 10054,
+    "description": "message size exceeds maximum allowed"
+  },
+  {
+    "constant": "JSStreamTemplateCreateErrF",
+    "code": 500,
+    "error_code": 10066,
+    "description": "{err}",
+    "comment": "Generic template creation failed string"
+  },
+  {
+    "constant": "JSInvalidJSONErr",
+    "code": 400,
+    "error_code": 10025,
+    "description": "invalid JSON"
+  },
+  {
+    "constant": "JSStreamInvalidExternalDeliverySubjErrF",
+    "code": 400,
+    "error_code": 10024,
+    "description": "stream external delivery prefix {prefix} must not contain wildcards"
+  },
+  {
+    "constant": "JSStreamRestoreErrF",
+    "code": 500,
+    "error_code": 10062,
+    "description": "restore failed: {err}"
+  },
+  {
+    "constant": "JSClusterIncompleteErr",
+    "code": 503,
+    "error_code": 10004,
+    "description": "incomplete results"
+  },
+  {
+    "constant": "JSNoAccountErr",
+    "code": 503,
+    "error_code": 10035,
+    "description": "account not found"
+  },
+  {
+    "constant": "JSRaftGeneralErrF",
+    "code": 500,
+    "error_code": 10041,
+    "description": "{err}",
+    "comment": "General RAFT error string"
+  },
+  {
+    "constant": "JSRestoreSubscribeFailedErrF",
+    "code": 500,
+    "error_code": 10042,
+    "description": "JetStream unable to subscribe to restore snapshot {subject}: {err}"
+  },
+  {
+    "constant": "JSStreamDeleteErrF",
+    "code": 500,
+    "error_code": 10050,
+    "description": "{err}",
+    "comment": "General stream deletion error string"
+  },
+  {
+    "constant": "JSStreamExternalApiOverlapErrF",
+    "code": 400,
+    "error_code": 10021,
+    "description": "stream external api prefix {prefix} must not overlap with {subject}"
+  },
+  {
+    "constant": "JSMirrorWithSubjectsErr",
+    "code": 400,
+    "error_code": 10034,
+    "description": "stream mirrors can not also contain subjects"
+  },
+  {
+    "constant": "JSNotEnabledErr",
+    "code": 503,
+    "error_code": 10076,
+    "description": "JetStream not enabled",
+    "help": "This error indicates that JetStream is not enabled at a global level",
+    "deprecates": "ErrJetStreamNotEnabled"
+  },
+  {
+    "constant": "JSNotEnabledForAccountErr",
+    "code": 503,
+    "error_code": 10039,
+    "description": "JetStream not enabled for account",
+    "help": "This error indicates that JetStream is not enabled for an account account level"
+  },
+  {
+    "constant": "JSSequenceNotFoundErrF",
+    "code": 400,
+    "error_code": 10043,
+    "description": "sequence {seq} not found"
+  },
+  {
+    "constant": "JSStreamMirrorNotUpdatableErr",
+    "code": 400,
+    "error_code": 10055,
+    "description": "Mirror configuration can not be updated"
+  },
+  {
+    "constant": "JSStreamSequenceNotMatchErr",
+    "code": 503,
+    "error_code": 10063,
+    "description": "expected stream sequence does not match"
+  },
+  {
+    "constant": "JSStreamWrongLastMsgIDErrF",
+    "code": 400,
+    "error_code": 10070,
+    "description": "wrong last msg ID: {id}"
+  },
+  {
+    "constant": "JSTempStorageFailedErr",
+    "code": 500,
+    "error_code": 10072,
+    "description": "JetStream unable to open temp storage for restore"
+  },
+  {
+    "constant": "JSStorageResourcesExceededErr",
+    "code": 500,
+    "error_code": 10047,
+    "description": "insufficient storage resources available",
+    "deprecates": "ErrStorageResourcesExceeded"
+  },
+  {
+    "constant": "JSStreamMismatchErr",
+    "code": 400,
+    "error_code": 10056,
+    "description": "stream name in subject does not match request"
+  },
+  {
+    "constant": "JSStreamNotMatchErr",
+    "code": 400,
+    "error_code": 10060,
+    "description": "expected stream does not match"
+  },
+  {
+    "constant": "JSMirrorConsumerSetupFailedErrF",
+    "code": 500,
+    "error_code": 10029,
+    "description": "{err}",
+    "comment": "Generic mirror consumer setup failure string"
+  },
+  {
+    "constant": "JSNotEmptyRequestErr",
+    "code": 400,
+    "error_code": 10038,
+    "description": "expected an empty request payload"
+  },
+  {
+    "constant": "JSStreamNameExistErr",
+    "code": 400,
+    "error_code": 10058,
+    "description": "stream name already in use",
+    "deprecates": "ErrJetStreamStreamAlreadyUsed"
+  },
+  {
+    "constant": "JSClusterTagsErr",
+    "code": 400,
+    "error_code": 10011,
+    "description": "tags placement not supported for operation"
+  },
+  {
+    "constant": "JSMaximumConsumersLimitErr",
+    "code": 400,
+    "error_code": 10026,
+    "description": "maximum consumers exceeds account limit"
+  },
+  {
+    "constant": "JSSourceConsumerSetupFailedErrF",
+    "code": 500,
+    "error_code": 10045,
+    "description": "{err}",
+    "comment": "General source consumer setup failure string"
+  },
+  {
+    "constant": "JSConsumerCreateErrF",
+    "code": 500,
+    "error_code": 10012,
+    "description": "{err}",
+    "comment": "General consumer creation failure string"
+  },
+  {
+    "constant": "JSConsumerDurableNameNotInSubjectErr",
+    "code": 400,
+    "error_code": 10016,
+    "description": "consumer expected to be durable but no durable name set in subject"
+  },
+  {
+    "constant": "JSStreamLimitsErrF",
+    "code": 500,
+    "error_code": 10053,
+    "description": "{err}",
+    "comment": "General stream limits exceeded error string"
+  },
+  {
+    "constant": "JSStreamReplicasNotUpdatableErr",
+    "code": 400,
+    "error_code": 10061,
+    "description": "Replicas configuration can not be updated"
+  },
+  {
+    "constant": "JSStreamTemplateNotFoundErr",
+    "code": 404,
+    "error_code": 10068,
+    "description": "template not found"
+  },
+  {
+    "constant": "JSClusterNotAssignedErr",
+    "code": 500,
+    "error_code": 10007,
+    "description": "JetStream cluster not assigned to this server",
+    "deprecates": "ErrJetStreamNotAssigned"
+  },
+  {
+    "constant": "JSClusterNotLeaderErr",
+    "code": 500,
+    "error_code": 10009,
+    "description": "JetStream cluster can not handle request",
+    "deprecates": "ErrJetStreamNotLeader"
+  },
+  {
+    "constant": "JSConsumerNameExistErr",
+    "code": 400,
+    "error_code": 10013,
+    "description": "consumer name already in use",
+    "deprecates": "ErrJetStreamConsumerAlreadyUsed"
+  },
+  {
+    "constant": "JSMirrorWithSourcesErr",
+    "code": 400,
+    "error_code": 10031,
+    "description": "stream mirrors can not also contain other sources"
+  },
+  {
+    "constant": "JSStreamNotFoundErr",
+    "code": 404,
+    "error_code": 10059,
+    "description": "stream not found",
+    "deprecates": "ErrJetStreamStreamNotFound"
+  },
+  {
+    "constant": "JSClusterRequiredErr",
+    "code": 503,
+    "error_code": 10010,
+    "description": "JetStream clustering support required"
+  },
+  {
+    "constant": "JSConsumerDurableNameNotSetErr",
+    "code": 400,
+    "error_code": 10018,
+    "description": "consumer expected to be durable but a durable name was not set"
+  },
+  {
+    "constant": "JSMaximumStreamsLimitErr",
+    "code": 400,
+    "error_code": 10027,
+    "description": "maximum number of streams reached"
+  },
+  {
+    "constant": "JSMirrorWithStartSeqAndTimeErr",
+    "code": 400,
+    "error_code": 10032,
+    "description": "stream mirrors can not have both start seq and start time configured"
+  },
+  {
+    "constant": "JSStreamSnapshotErrF",
+    "code": 500,
+    "error_code": 10064,
+    "description": "snapshot failed: {err}"
+  },
+  {
+    "constant": "JSStreamUpdateErrF",
+    "code": 500,
+    "error_code": 10069,
+    "description": "{err}",
+    "comment": "Generic stream update error string"
+  },
+  {
+    "constant": "JSClusterNotActiveErr",
+    "code": 500,
+    "error_code": 10006,
+    "description": "JetStream not in clustered mode",
+    "deprecates": "ErrJetStreamNotClustered"
+  },
+  {
+    "constant": "JSConsumerDurableNameNotMatchSubjectErr",
+    "code": 400,
+    "error_code": 10017,
+    "description": "consumer name in subject does not match durable name in request"
+  },
+  {
+    "constant": "JSMemoryResourcesExceededErr",
+    "code": 500,
+    "error_code": 10028,
+    "description": "insufficient memory resources available",
+    "deprecates": "ErrMemoryResourcesExceeded"
+  },
+  {
+    "constant": "JSMirrorWithSubjectFiltersErr",
+    "code": 400,
+    "error_code": 10033,
+    "description": "stream mirrors can not contain filtered subjects"
+  },
+  {
+    "constant": "JSStreamCreateErrF",
+    "code": 500,
+    "error_code": 10049,
+    "description": "{err}",
+    "comment": "Generic stream creation error string"
+  },
+  {
+    "constant": "JSClusterServerNotMemberErr",
+    "code": 400,
+    "error_code": 10044,
+    "description": "server is not a member of the cluster"
+  },
+  {
+    "constant": "JSNoMessageFoundErr",
+    "code": 404,
+    "error_code": 10037,
+    "description": "no message found"
+  },
+  {
+    "constant": "JSSnapshotDeliverSubjectInvalidErr",
+    "code": 400,
+    "error_code": 10015,
+    "description": "deliver subject not valid"
+  },
+  {
+    "constant": "JSStreamGeneralErrorF",
+    "code": 500,
+    "error_code": 10051,
+    "description": "General stream failure string"
+  },
+  {
+    "constant": "JSStreamInvalidConfigF",
+    "code": 500,
+    "error_code": 10052,
+    "description": "{err}",
+    "comment": "Stream configuration validation error string"
+  },
+  {
+    "constant": "JSStreamReplicasNotSupportedErr",
+    "code": 500,
+    "error_code": 10074,
+    "description": "replicas > 1 not supported in non-clustered mode",
+    "deprecates": "ErrReplicasNotSupported"
+  },
+  {
+    "constant": "JSStreamMsgDeleteFailedF",
+    "code": 500,
+    "error_code": 10057,
+    "description": "%s",
+    "comment": "Generic message deletion failure error string"
+  },
+  {
+    "constant": "JSPeerRemapErr",
+    "code": 503,
+    "error_code": 10075,
+    "description": "peer remap failed"
+  }
+]
diff --git a/server/errors_gen.go b/server/errors_gen.go
new file mode 100644
index 00000000..b7fa32a3
--- /dev/null
+++ b/server/errors_gen.go
@@ -0,0 +1,128 @@
+// +build ignore
+
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"sort"
+	"text/template"
+)
+
+var templ = `
+// Generated code, do not edit. See errors.json and run go generate to update
+
+package server
+
+const (
+{{- range $i, $error := . }}
+{{- if .Comment }}
+	// {{ .Constant }} {{ .Comment }} ({{ .Description | print }})
+{{- else }}
+	// {{ .Constant }} {{ .Description | print }}
+{{- end }}
+	{{ .Constant }} ErrorIdentifier = {{ .ErrCode }}  
+{{ end }}
+)
+
+var (
+	ApiErrors = map[ErrorIdentifier]*ApiError{
+{{- range $i, $error := . }}
+		{{ .Constant }}: {Code: {{ .Code }},ErrCode: {{ .ErrCode }},Description: {{ .Description | printf "%q" }},{{- if .Help }}Help: {{ .Help | printf "%q" }},{{- end }}{{- if .URL }}URL: {{ .URL | printf "%q" }},{{- end }}	},{{- end }}
+	}
+
+{{- range $i, $error := . }}
+{{- if .Deprecates }}
+// {{ .Deprecates }} Deprecated by {{ .Constant }} ApiError, use IsNatsError() for comparisons
+{{ .Deprecates }} = ApiErrors[{{ .Constant }}]
+{{- end }}
+{{- end }}
+)
+`
+
+func panicIfErr(err error) {
+	if err == nil {
+		return
+	}
+	panic(err)
+}
+
+type Errors struct {
+	Constant    string `json:"constant"`
+	Code        int    `json:"code"`
+	ErrCode     int    `json:"error_code"`
+	Description string `json:"description"`
+	Comment     string `json:"comment"`
+	Help        string `json:"help"`
+	URL         string `json:"url"`
+	Deprecates  string `json:"deprecates"`
+}
+
+func goFmt(file string) error {
+	c := exec.Command("go", "fmt", file)
+	out, err := c.CombinedOutput()
+	if err != nil {
+		log.Printf("go fmt failed: %s", string(out))
+	}
+
+	return err
+}
+
+func checkDupes(errs []Errors) error {
+	codes := []int{}
+	highest := 0
+	for _, err := range errs {
+		codes = append(codes, err.ErrCode)
+		if highest < err.ErrCode {
+			highest = err.ErrCode
+		}
+	}
+
+	codeKeys := make(map[int]bool)
+	constKeys := make(map[string]bool)
+
+	for _, entry := range errs {
+		if _, found := codeKeys[entry.ErrCode]; found {
+			return fmt.Errorf("duplicate error code %+v, highest code is %d", entry, highest)
+		}
+
+		if _, found := constKeys[entry.Constant]; found {
+			return fmt.Errorf("duplicate error constant %+v", entry)
+		}
+
+		codeKeys[entry.ErrCode] = true
+		constKeys[entry.Constant] = true
+	}
+
+	return nil
+}
+
+func main() {
+	ej, err := os.ReadFile("server/errors.json")
+	panicIfErr(err)
+
+	errs := []Errors{}
+	panicIfErr(json.Unmarshal(ej, &errs))
+	panicIfErr(checkDupes(errs))
+
+	sort.Slice(errs, func(i, j int) bool {
+		return errs[i].Constant < errs[j].Constant
+	})
+
+	t := template.New("errors").Funcs(template.FuncMap{"inc": func(i int) int { return i + 1 }})
+	p, err := t.Parse(templ)
+	panicIfErr(err)
+
+	tf, err := ioutil.TempFile("", "")
+	panicIfErr(err)
+	defer tf.Close()
+
+	panicIfErr(p.Execute(tf, errs))
+
+	panicIfErr(os.Rename(tf.Name(), "server/jetstream_errors_generated.go"))
+	panicIfErr(goFmt("server/jetstream_errors_generated.go"))
+}
diff --git a/server/jetstream.go b/server/jetstream.go
index ff132ce6..78f2d564 100644
--- a/server/jetstream.go
+++ b/server/jetstream.go
@@ -775,7 +775,7 @@ func (s *Server) JetStreamNumAccounts() int {
 func (s *Server) JetStreamReservedResources() (int64, int64, error) {
 	js := s.getJetStream()
 	if js == nil {
-		return -1, -1, ErrJetStreamNotEnabled
+		return -1, -1, ApiErrors[JSNotEnabledForAccountErr]
 	}
 	js.mu.RLock()
 	defer js.mu.RUnlock()
@@ -818,7 +818,7 @@ func (a *Account) EnableJetStream(limits *JetStreamAccountLimits) error {
 			limits = dynamicJSAccountLimits
 		}
 		a.assignJetStreamLimits(limits)
-		return ErrJetStreamNotEnabled
+		return ApiErrors[JSNotEnabledErr]
 	}
 
 	if s.SystemAccount() == a {
@@ -1107,14 +1107,14 @@ func (a *Account) lookupStream(name string) (*stream, error) {
 	a.mu.RUnlock()
 
 	if jsa == nil {
-		return nil, ErrJetStreamNotEnabled
+		return nil, ApiErrors[JSNotEnabledForAccountErr]
 	}
 	jsa.mu.Lock()
 	defer jsa.mu.Unlock()
 
 	mset, ok := jsa.streams[name]
 	if !ok {
-		return nil, ErrJetStreamStreamNotFound
+		return nil, ApiErrors[JSStreamNotFoundErr]
 	}
 	return mset, nil
 }
@@ -1131,10 +1131,10 @@ func (a *Account) UpdateJetStreamLimits(limits *JetStreamAccountLimits) error {
 	}
 	js := s.getJetStream()
 	if js == nil {
-		return ErrJetStreamNotEnabled
+		return ApiErrors[JSNotEnabledErr]
 	}
 	if jsa == nil {
-		return ErrJetStreamNotEnabledForAccount
+		return ApiErrors[JSNotEnabledForAccountErr]
 	}
 
 	if limits == nil {
@@ -1223,7 +1223,7 @@ func (a *Account) DisableJetStream() error {
 
 	js := s.getJetStream()
 	if js == nil {
-		return ErrJetStreamNotEnabled
+		return ApiErrors[JSNotEnabledForAccountErr]
 	}
 
 	// Remove service imports.
@@ -1248,7 +1248,7 @@ func (a *Account) removeJetStream() error {
 
 	js := s.getJetStream()
 	if js == nil {
-		return ErrJetStreamNotEnabled
+		return ApiErrors[JSNotEnabledForAccountErr]
 	}
 
 	return js.disableJetStream(js.lookupAccount(a))
@@ -1257,7 +1257,7 @@ func (a *Account) removeJetStream() error {
 // Disable JetStream for the account.
 func (js *jetStream) disableJetStream(jsa *jsAccount) error {
 	if jsa == nil || jsa.account == nil {
-		return ErrJetStreamNotEnabledForAccount
+		return ApiErrors[JSNotEnabledForAccountErr]
 	}
 
 	js.mu.Lock()
@@ -1428,11 +1428,11 @@ func (jsa *jsAccount) limitsExceeded(storeType StorageType) bool {
 // Lock should be held.
 func (jsa *jsAccount) checkLimits(config *StreamConfig) error {
 	if jsa.limits.MaxStreams > 0 && len(jsa.streams) >= jsa.limits.MaxStreams {
-		return fmt.Errorf("maximum number of streams reached")
+		return ApiErrors[JSMaximumStreamsLimitErr]
 	}
 	// Check MaxConsumers
 	if config.MaxConsumers > 0 && jsa.limits.MaxConsumers > 0 && config.MaxConsumers > jsa.limits.MaxConsumers {
-		return fmt.Errorf("maximum consumers exceeds account limit")
+		return ApiErrors[JSMaximumConsumersLimitErr]
 	}
 
 	// Check storage, memory or disk.
@@ -1451,26 +1451,26 @@ func (jsa *jsAccount) checkBytesLimits(addBytes int64, storage StorageType) erro
 		// Account limits defined.
 		if jsa.limits.MaxMemory > 0 {
 			if jsa.memReserved+addBytes > jsa.limits.MaxMemory {
-				return ErrMemoryResourcesExceeded
+				return ApiErrors[JSMemoryResourcesExceededErr]
 			}
 		} else {
 			// Account is unlimited, check if this server can handle request.
 			js := jsa.js
 			if js.memReserved+addBytes > js.config.MaxMemory {
-				return ErrMemoryResourcesExceeded
+				return ApiErrors[JSMemoryResourcesExceededErr]
 			}
 		}
 	case FileStorage:
 		// Account limits defined.
 		if jsa.limits.MaxStore > 0 {
 			if jsa.storeReserved+addBytes > jsa.limits.MaxStore {
-				return ErrStorageResourcesExceeded
+				return ApiErrors[JSStorageResourcesExceededErr]
 			}
 		} else {
 			// Account is unlimited, check if this server can handle request.
 			js := jsa.js
 			if js.storeReserved+addBytes > js.config.MaxStore {
-				return ErrStorageResourcesExceeded
+				return ApiErrors[JSStorageResourcesExceededErr]
 			}
 		}
 	}
@@ -1573,10 +1573,10 @@ func (js *jetStream) sufficientResources(limits *JetStreamAccountLimits) error {
 	}
 
 	if js.memReserved+limits.MaxMemory > js.config.MaxMemory {
-		return ErrMemoryResourcesExceeded
+		return ApiErrors[JSMemoryResourcesExceededErr]
 	}
 	if js.storeReserved+limits.MaxStore > js.config.MaxStore {
-		return ErrStorageResourcesExceeded
+		return ApiErrors[JSStorageResourcesExceededErr]
 	}
 	return nil
 }
@@ -1664,7 +1664,7 @@ func (a *Account) checkForJetStream() (*Server, *jsAccount, error) {
 	a.mu.RUnlock()
 
 	if s == nil || jsa == nil {
-		return nil, nil, ErrJetStreamNotEnabledForAccount
+		return nil, nil, ApiErrors[JSNotEnabledForAccountErr]
 	}
 
 	return s, jsa, nil
@@ -1875,7 +1875,7 @@ func (t *streamTemplate) delete() error {
 	t.mu.Unlock()
 
 	if jsa == nil {
-		return ErrJetStreamNotEnabled
+		return ApiErrors[JSNotEnabledForAccountErr]
 	}
 
 	jsa.mu.Lock()
@@ -1919,7 +1919,7 @@ func (t *streamTemplate) delete() error {
 func (a *Account) deleteStreamTemplate(name string) error {
 	t, err := a.lookupStreamTemplate(name)
 	if err != nil {
-		return err
+		return ApiErrors[JSStreamTemplateNotFoundErr]
 	}
 	return t.delete()
 }
diff --git a/server/jetstream_api.go b/server/jetstream_api.go
index c1cce472..978fc933 100644
--- a/server/jetstream_api.go
+++ b/server/jetstream_api.go
@@ -39,10 +39,9 @@ const (
 	// For constructing JetStream domain prefixes.
 	jsDomainAPI = "$JS.%s.API.>"
 
-	// JSApiPrefix
 	JSApiPrefix = "$JS.API"
 
-	// JSApiInfo is for obtaining general information about JetStream for this account.
+	// JSApiAccountInfo is for obtaining general information about JetStream for this account.
 	// Will return JSON response.
 	JSApiAccountInfo = "$JS.API.INFO"
 
@@ -91,7 +90,7 @@ const (
 	JSApiStreamDelete  = "$JS.API.STREAM.DELETE.*"
 	JSApiStreamDeleteT = "$JS.API.STREAM.DELETE.%s"
 
-	// JSApiPurgeStream is the endpoint to purge streams.
+	// JSApiStreamPurge is the endpoint to purge streams.
 	// Will return JSON response.
 	JSApiStreamPurge  = "$JS.API.STREAM.PURGE.*"
 	JSApiStreamPurgeT = "$JS.API.STREAM.PURGE.%s"
@@ -104,11 +103,11 @@ const (
 	JSApiStreamSnapshotT = "$JS.API.STREAM.SNAPSHOT.%s"
 
 	// JSApiStreamRestore is the endpoint to restore a stream from a snapshot.
-	// Caller should resond to each chunk with a nil body response.
+	// Caller should respond to each chunk with a nil body response.
 	JSApiStreamRestore  = "$JS.API.STREAM.RESTORE.*"
 	JSApiStreamRestoreT = "$JS.API.STREAM.RESTORE.%s"
 
-	// JSApiDeleteMsg is the endpoint to delete messages from a stream.
+	// JSApiMsgDelete is the endpoint to delete messages from a stream.
 	// Will return JSON response.
 	JSApiMsgDelete  = "$JS.API.STREAM.MSG.DELETE.*"
 	JSApiMsgDeleteT = "$JS.API.STREAM.MSG.DELETE.%s"
@@ -134,15 +133,14 @@ const (
 	JSApiConsumersT = "$JS.API.CONSUMER.NAMES.%s"
 
 	// JSApiConsumerList is the endpoint that will return all detailed consumer information
-	JSApiConsumerList  = "$JS.API.CONSUMER.LIST.*"
-	JSApiConsumerListT = "$JS.API.CONSUMER.LIST.%s"
+	JSApiConsumerList = "$JS.API.CONSUMER.LIST.*"
 
 	// JSApiConsumerInfo is for obtaining general information about a consumer.
 	// Will return JSON response.
 	JSApiConsumerInfo  = "$JS.API.CONSUMER.INFO.*.*"
 	JSApiConsumerInfoT = "$JS.API.CONSUMER.INFO.%s.%s"
 
-	// JSApiDeleteConsumer is the endpoint to delete consumers.
+	// JSApiConsumerDelete is the endpoint to delete consumers.
 	// Will return JSON response.
 	JSApiConsumerDelete  = "$JS.API.CONSUMER.DELETE.*.*"
 	JSApiConsumerDeleteT = "$JS.API.CONSUMER.DELETE.%s.%s"
@@ -231,13 +229,13 @@ const (
 	// JSAdvisoryStreamRestoreCompletePre notification that a restore was completed.
 	JSAdvisoryStreamRestoreCompletePre = "$JS.EVENT.ADVISORY.STREAM.RESTORE_COMPLETE"
 
-	// JSAdvisoryStreamLeaderElectPre notification that a replicated stream has elected a leader.
+	// JSAdvisoryStreamLeaderElectedPre notification that a replicated stream has elected a leader.
 	JSAdvisoryStreamLeaderElectedPre = "$JS.EVENT.ADVISORY.STREAM.LEADER_ELECTED"
 
 	// JSAdvisoryStreamQuorumLostPre notification that a stream and its consumers are stalled.
 	JSAdvisoryStreamQuorumLostPre = "$JS.EVENT.ADVISORY.STREAM.QUORUM_LOST"
 
-	// JSAdvisoryConsumerLeaderElectPre notification that a replicated consumer has elected a leader.
+	// JSAdvisoryConsumerLeaderElectedPre notification that a replicated consumer has elected a leader.
 	JSAdvisoryConsumerLeaderElectedPre = "$JS.EVENT.ADVISORY.CONSUMER.LEADER_ELECTED"
 
 	// JSAdvisoryConsumerQuorumLostPre notification that a consumer is stalled.
@@ -254,24 +252,26 @@ const (
 	JSAuditAdvisory = "$JS.EVENT.ADVISORY.API"
 )
 
-// Maximum name lengths for streams, consumers and templates.
+// JSMaxNameLen is the maximum name lengths for streams, consumers and templates.
 const JSMaxNameLen = 256
 
 // Responses for API calls.
 
-// ApiError is included in all responses if there was an error.
-// TODO(dlc) - Move to more generic location.
-type ApiError struct {
-	Code        int    `json:"code"`
-	Description string `json:"description,omitempty"`
-}
-
 // ApiResponse is a standard response from the JetStream JSON API
 type ApiResponse struct {
 	Type  string    `json:"type"`
 	Error *ApiError `json:"error,omitempty"`
 }
 
+// ToError checks if the response has a error and if it does converts it to an error avoiding the pitfalls described by https://yourbasic.org/golang/gotcha-why-nil-error-not-equal-nil/
+func (r *ApiResponse) ToError() error {
+	if r.Error == nil {
+		return nil
+	}
+
+	return r.Error
+}
+
 const JSApiOverloadedType = "io.nats.jetstream.api.v1.system_overloaded"
 
 // ApiPaged includes variables used to create paged responses from the JSON API
@@ -310,12 +310,10 @@ type JSApiStreamDeleteResponse struct {
 
 const JSApiStreamDeleteResponseType = "io.nats.jetstream.api.v1.stream_delete_response"
 
-// JSApiStreamInfoRequest.
 type JSApiStreamInfoRequest struct {
 	DeletedDetails bool `json:"deleted_details,omitempty"`
 }
 
-// JSApiStreamInfoResponse.
 type JSApiStreamInfoResponse struct {
 	ApiResponse
 	*StreamInfo
@@ -323,7 +321,7 @@ type JSApiStreamInfoResponse struct {
 
 const JSApiStreamInfoResponseType = "io.nats.jetstream.api.v1.stream_info_response"
 
-// Maximum entries we will return for streams or consumers lists.
+// JSApiNamesLimit is the maximum entries we will return for streams or consumers lists.
 // TODO(dlc) - with header or request support could request chunked response.
 const JSApiNamesLimit = 1024
 const JSApiListLimit = 256
@@ -354,7 +352,6 @@ type JSApiStreamListResponse struct {
 
 const JSApiStreamListResponseType = "io.nats.jetstream.api.v1.stream_list_response"
 
-// JSApiStreamPurgeResponse.
 type JSApiStreamPurgeResponse struct {
 	ApiResponse
 	Success bool   `json:"success,omitempty"`
@@ -377,7 +374,6 @@ type JSApiMsgDeleteRequest struct {
 	NoErase bool   `json:"no_erase,omitempty"`
 }
 
-// JSApiMsgDeleteResponse.
 type JSApiMsgDeleteResponse struct {
 	ApiResponse
 	Success bool `json:"success,omitempty"`
@@ -416,8 +412,6 @@ type JSApiStreamRestoreRequest struct {
 	State StreamState `json:"state"`
 }
 
-const JSApiStreamRestoreRequestType = "io.nats.jetstream.api.v1.stream_restore_request"
-
 // JSApiStreamRestoreResponse is the direct response to the restore request.
 type JSApiStreamRestoreResponse struct {
 	ApiResponse
@@ -489,7 +483,6 @@ type JSApiMsgGetRequest struct {
 	Seq uint64 `json:"seq"`
 }
 
-// JSApiMsgGetResponse.
 type JSApiMsgGetResponse struct {
 	ApiResponse
 	Message *StoredMsg `json:"message,omitempty"`
@@ -500,7 +493,6 @@ const JSApiMsgGetResponseType = "io.nats.jetstream.api.v1.stream_msg_get_respons
 // JSWaitQueueDefaultMax is the default max number of outstanding requests for pull consumers.
 const JSWaitQueueDefaultMax = 512
 
-// JSApiConsumerCreateResponse.
 type JSApiConsumerCreateResponse struct {
 	ApiResponse
 	*ConsumerInfo
@@ -508,7 +500,6 @@ type JSApiConsumerCreateResponse struct {
 
 const JSApiConsumerCreateResponseType = "io.nats.jetstream.api.v1.consumer_create_response"
 
-// JSApiConsumerDeleteResponse.
 type JSApiConsumerDeleteResponse struct {
 	ApiResponse
 	Success bool `json:"success,omitempty"`
@@ -516,7 +507,6 @@ type JSApiConsumerDeleteResponse struct {
 
 const JSApiConsumerDeleteResponseType = "io.nats.jetstream.api.v1.consumer_delete_response"
 
-// JSApiConsumerInfoResponse.
 type JSApiConsumerInfoResponse struct {
 	ApiResponse
 	*ConsumerInfo
@@ -524,12 +514,10 @@ type JSApiConsumerInfoResponse struct {
 
 const JSApiConsumerInfoResponseType = "io.nats.jetstream.api.v1.consumer_info_response"
 
-// JSApiConsumersRequest
 type JSApiConsumersRequest struct {
 	ApiPagedRequest
 }
 
-// JSApiConsumerNamesResponse.
 type JSApiConsumerNamesResponse struct {
 	ApiResponse
 	ApiPaged
@@ -538,7 +526,6 @@ type JSApiConsumerNamesResponse struct {
 
 const JSApiConsumerNamesResponseType = "io.nats.jetstream.api.v1.consumer_names_response"
 
-// JSApiConsumerListResponse.
 type JSApiConsumerListResponse struct {
 	ApiResponse
 	ApiPaged
@@ -562,7 +549,6 @@ type JSApiStreamTemplateCreateResponse struct {
 
 const JSApiStreamTemplateCreateResponseType = "io.nats.jetstream.api.v1.stream_template_create_response"
 
-// JSApiStreamTemplateDeleteResponse
 type JSApiStreamTemplateDeleteResponse struct {
 	ApiResponse
 	Success bool `json:"success,omitempty"`
@@ -578,7 +564,6 @@ type JSApiStreamTemplateInfoResponse struct {
 
 const JSApiStreamTemplateInfoResponseType = "io.nats.jetstream.api.v1.stream_template_info_response"
 
-// JSApiStreamTemplatesRequest
 type JSApiStreamTemplatesRequest struct {
 	ApiPagedRequest
 }
@@ -592,27 +577,6 @@ type JSApiStreamTemplateNamesResponse struct {
 
 const JSApiStreamTemplateNamesResponseType = "io.nats.jetstream.api.v1.stream_template_names_response"
 
-var (
-	jsNotEnabledErr        = &ApiError{Code: 503, Description: "JetStream not enabled for account"}
-	jsBadRequestErr        = &ApiError{Code: 400, Description: "bad request"}
-	jsNotEmptyRequestErr   = &ApiError{Code: 400, Description: "expected an empty request payload"}
-	jsInvalidJSONErr       = &ApiError{Code: 400, Description: "invalid JSON"}
-	jsInsufficientErr      = &ApiError{Code: 503, Description: "insufficient resources"}
-	jsNoConsumerErr        = &ApiError{Code: 404, Description: "consumer not found"}
-	jsStreamMismatchErr    = &ApiError{Code: 400, Description: "stream name in subject does not match request"}
-	jsNoClusterSupportErr  = &ApiError{Code: 503, Description: "not currently supported in clustered mode"}
-	jsClusterNotAvailErr   = &ApiError{Code: 503, Description: "JetStream system temporarily unavailable"}
-	jsClusterRequiredErr   = &ApiError{Code: 503, Description: "JetStream clustering support required"}
-	jsPeerNotMemberErr     = &ApiError{Code: 400, Description: "peer not a member"}
-	jsPeerRemapErr         = &ApiError{Code: 503, Description: "peer remap failed"}
-	jsClusterIncompleteErr = &ApiError{Code: 503, Description: "incomplete results"}
-	jsClusterTagsErr       = &ApiError{Code: 400, Description: "tags placement not supported for operation"}
-	jsClusterNoPeersErr    = &ApiError{Code: 400, Description: "no suitable peers for placement"}
-	jsServerNotMemberErr   = &ApiError{Code: 400, Description: "server is not a member of the cluster"}
-	jsNoMessageFoundErr    = &ApiError{Code: 404, Description: "no message found"}
-	jsNoAccountErr         = &ApiError{Code: 503, Description: "account not found"}
-)
-
 // For easier handling of exports and imports.
 var allJsExports = []string{
 	JSApiAccountInfo,
@@ -678,7 +642,7 @@ func (js *jetStream) apiDispatch(sub *subscription, c *client, subject, reply st
 		atomic.AddInt64(&js.apiCalls, -1)
 		ci, acc, _, msg, err := s.getRequestInfo(c, rmsg)
 		if err == nil {
-			resp := &ApiResponse{Type: JSApiOverloadedType, Error: jsInsufficientErr}
+			resp := &ApiResponse{Type: JSApiOverloadedType, Error: ApiErrors[JSInsufficientResourcesErr]}
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		} else {
 			s.Warnf(badAPIRequestT, rmsg)
@@ -707,7 +671,7 @@ func (js *jetStream) apiDispatch(sub *subscription, c *client, subject, reply st
 func (s *Server) setJetStreamExportSubs() error {
 	js := s.getJetStream()
 	if js == nil {
-		return ErrJetStreamNotEnabled
+		return ApiErrors[JSNotEnabledErr]
 	}
 
 	// This is the catch all now for all JetStream API calls.
@@ -889,7 +853,7 @@ func (s *Server) jsAccountInfoRequest(sub *subscription, c *client, subject, rep
 			return
 		}
 		if js.isLeaderless() {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -903,7 +867,7 @@ func (s *Server) jsAccountInfoRequest(sub *subscription, c *client, subject, rep
 		if !doErr {
 			return
 		}
-		resp.Error = jsNotEnabledErr
+		resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 	} else {
 		stats := acc.JetStreamUsage()
 		resp.JetStreamAccountStats = &stats
@@ -941,34 +905,34 @@ func (s *Server) jsTemplateCreateRequest(sub *subscription, c *client, subject,
 
 	var resp = JSApiStreamTemplateCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateCreateResponseType}}
 	if !acc.JetStreamEnabled() {
-		resp.Error = jsNotEnabledErr
+		resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	// Not supported for now.
 	if s.JetStreamIsClustered() {
-		resp.Error = jsNoClusterSupportErr
+		resp.Error = ApiErrors[JSClusterUnSupportFeatureErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	var cfg StreamTemplateConfig
 	if err := json.Unmarshal(msg, &cfg); err != nil {
-		resp.Error = jsInvalidJSONErr
+		resp.Error = ApiErrors[JSInvalidJSONErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	templateName := templateNameFromSubject(subject)
 	if templateName != cfg.Name {
-		resp.Error = &ApiError{Code: 400, Description: "template name in subject does not match request"}
+		resp.Error = ApiErrors[JSTemplateNameNotMatchSubjectErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	t, err := acc.addStreamTemplate(&cfg)
 	if err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSStreamTemplateCreateErrF].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -996,14 +960,14 @@ func (s *Server) jsTemplateNamesRequest(sub *subscription, c *client, subject, r
 
 	var resp = JSApiStreamTemplateNamesResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateNamesResponseType}}
 	if !acc.JetStreamEnabled() {
-		resp.Error = jsNotEnabledErr
+		resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	// Not supported for now.
 	if s.JetStreamIsClustered() {
-		resp.Error = jsNoClusterSupportErr
+		resp.Error = ApiErrors[JSClusterUnSupportFeatureErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1012,7 +976,7 @@ func (s *Server) jsTemplateNamesRequest(sub *subscription, c *client, subject, r
 	if !isEmptyRequest(msg) {
 		var req JSApiStreamTemplatesRequest
 		if err := json.Unmarshal(msg, &req); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -1060,19 +1024,19 @@ func (s *Server) jsTemplateInfoRequest(sub *subscription, c *client, subject, re
 
 	var resp = JSApiStreamTemplateInfoResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateInfoResponseType}}
 	if !acc.JetStreamEnabled() {
-		resp.Error = jsNotEnabledErr
+		resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	if !isEmptyRequest(msg) {
-		resp.Error = jsNotEmptyRequestErr
+		resp.Error = ApiErrors[JSNotEmptyRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	name := templateNameFromSubject(subject)
 	t, err := acc.lookupStreamTemplate(name)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamTemplateNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1101,19 +1065,19 @@ func (s *Server) jsTemplateDeleteRequest(sub *subscription, c *client, subject,
 
 	var resp = JSApiStreamTemplateDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamTemplateDeleteResponseType}}
 	if !acc.JetStreamEnabled() {
-		resp.Error = jsNotEnabledErr
+		resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	if !isEmptyRequest(msg) {
-		resp.Error = jsNotEmptyRequestErr
+		resp.Error = ApiErrors[JSNotEmptyRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	name := templateNameFromSubject(subject)
 	err = acc.deleteStreamTemplate(name)
 	if err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSStreamTemplateDeleteErrF].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1130,20 +1094,6 @@ func (s *Server) jsonResponse(v interface{}) string {
 	return string(b)
 }
 
-func jsError(err error) *ApiError {
-	return &ApiError{
-		Code:        500,
-		Description: err.Error(),
-	}
-}
-
-func jsNotFoundError(err error) *ApiError {
-	return &ApiError{
-		Code:        404,
-		Description: err.Error(),
-	}
-}
-
 // Request to create a stream.
 func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, reply string, rmsg []byte) {
 	if c == nil || !s.JetStreamEnabled() {
@@ -1164,7 +1114,7 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re
 			return
 		}
 		if js.isLeaderless() {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -1176,20 +1126,20 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
 	}
 	var cfg StreamConfig
 	if err := json.Unmarshal(msg, &cfg); err != nil {
-		resp.Error = jsInvalidJSONErr
+		resp.Error = ApiErrors[JSInvalidJSONErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	streamName := streamNameFromSubject(subject)
 	if streamName != cfg.Name {
-		resp.Error = jsStreamMismatchErr
+		resp.Error = ApiErrors[JSStreamMismatchErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1223,22 +1173,22 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re
 	// Do some pre-checking for mirror config to avoid cycles in clustered mode.
 	if cfg.Mirror != nil {
 		if len(cfg.Subjects) > 0 {
-			resp.Error = &ApiError{Code: 400, Description: "stream mirrors can not also contain subjects"}
+			resp.Error = ApiErrors[JSMirrorWithSubjectsErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
 		if len(cfg.Sources) > 0 {
-			resp.Error = &ApiError{Code: 400, Description: "stream mirrors can not also contain other sources"}
+			resp.Error = ApiErrors[JSMirrorWithSourcesErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
 		if cfg.Mirror.FilterSubject != _EMPTY_ {
-			resp.Error = &ApiError{Code: 400, Description: "stream mirrors can not contain filtered subjects"}
+			resp.Error = ApiErrors[JSMirrorWithSubjectFiltersErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
 		if cfg.Mirror.OptStartSeq > 0 && cfg.Mirror.OptStartTime != nil {
-			resp.Error = &ApiError{Code: 400, Description: "stream mirrors can not have both start seq and start time configured"}
+			resp.Error = ApiErrors[JSMirrorWithStartSeqAndTimeErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -1253,7 +1203,7 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re
 			streamSubs = append(streamSubs, subs...)
 		}
 		if exists && cfg.MaxMsgSize > 0 && maxMsgSize > 0 && cfg.MaxMsgSize < maxMsgSize {
-			resp.Error = &ApiError{Code: 400, Description: "stream mirror must have max message size >= source"}
+			resp.Error = ApiErrors[JSMirrorMaxMessageSizeTooBigErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -1282,7 +1232,7 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re
 				apiPrefixes = append(apiPrefixes, src.External.ApiPrefix)
 			}
 			if exists && cfg.MaxMsgSize > 0 && maxMsgSize > 0 && cfg.MaxMsgSize < maxMsgSize {
-				resp.Error = &ApiError{Code: 400, Description: "stream source must have max message size >= target"}
+				resp.Error = ApiErrors[JSSourceMaxMessageSizeTooBigErr]
 				s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 				return
 			}
@@ -1291,13 +1241,13 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re
 	// check prefix overlap with subjects
 	for _, pfx := range deliveryPrefixes {
 		if !IsValidPublishSubject(pfx) {
-			resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("stream external delivery prefix %q must be a valid subject without wildcards", pfx)}
+			resp.Error = ApiErrors[JSStreamInvalidExternalDeliverySubjErrF].NewT("{prefix}", pfx)
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
 		for _, sub := range streamSubs {
 			if SubjectsCollide(sub, fmt.Sprintf("%s.%s", pfx, sub)) {
-				resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("stream external delivery prefix %q overlaps with stream subject %q", pfx, sub)}
+				resp.Error = ApiErrors[JSStreamExternalDelPrefixOverlapsErrF].NewT("{prefix}", pfx, "{subject}", sub)
 				s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 				return
 			}
@@ -1311,7 +1261,7 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re
 			return
 		}
 		if SubjectsCollide(apiPfx, JSApiPrefix) {
-			resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("stream external api prefix %q must not overlap with %s", apiPfx, JSApiPrefix)}
+			resp.Error = ApiErrors[JSStreamExternalApiOverlapErrF].NewT("{prefix}", apiPfx, "{subject}", JSApiPrefix)
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -1324,7 +1274,7 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, subject, re
 
 	mset, err := acc.addStream(&cfg)
 	if err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSStreamCreateErrF].NewT("{err}", err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1353,7 +1303,7 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, subject, re
 			return
 		}
 		if js.isLeaderless() {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -1365,28 +1315,28 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, subject, re
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
 	}
 	var ncfg StreamConfig
 	if err := json.Unmarshal(msg, &ncfg); err != nil {
-		resp.Error = jsInvalidJSONErr
+		resp.Error = ApiErrors[JSInvalidJSONErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	cfg, err := checkStreamCfg(&ncfg)
 	if err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSStreamInvalidConfigF].NewT("{err}", err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	streamName := streamNameFromSubject(subject)
 	if streamName != cfg.Name {
-		resp.Error = jsStreamMismatchErr
+		resp.Error = ApiErrors[JSStreamMismatchErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1398,13 +1348,13 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, subject, re
 
 	mset, err := acc.lookupStream(streamName)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	if err := mset.update(&cfg); err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSStreamUpdateErrF].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1435,7 +1385,7 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, subject, rep
 			return
 		}
 		if js.isLeaderless() {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -1447,7 +1397,7 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, subject, rep
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
@@ -1459,7 +1409,7 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, subject, rep
 	if !isEmptyRequest(msg) {
 		var req JSApiStreamNamesRequest
 		if err := json.Unmarshal(msg, &req); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -1480,7 +1430,7 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, subject, rep
 		}
 		js.mu.RLock()
 		for stream, sa := range cc.streams[acc.Name] {
-			if sa.err == ErrJetStreamNotAssigned {
+			if sa.err == ApiErrors[JSClusterNotAssignedErr] {
 				continue
 			}
 			if filter != _EMPTY_ {
@@ -1562,7 +1512,7 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, subject, repl
 			return
 		}
 		if js.isLeaderless() {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -1574,7 +1524,7 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, subject, repl
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
@@ -1584,7 +1534,7 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, subject, repl
 	if !isEmptyRequest(msg) {
 		var req JSApiStreamNamesRequest
 		if err := json.Unmarshal(msg, &req); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -1660,18 +1610,18 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, subject, repl
 			// We can't find the stream, so mimic what would be the errors below.
 			if hasJS, doErr := acc.checkJetStream(); !hasJS {
 				if doErr {
-					resp.Error = jsNotEnabledErr
+					resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 					s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 				}
 				return
 			}
 			// No stream present.
-			resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+			resp.Error = ApiErrors[JSStreamNotFoundErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		} else if sa == nil {
 			if js.isLeaderless() {
-				resp.Error = jsClusterNotAvailErr
+				resp.Error = ApiErrors[JSClusterNotAvailErr]
 				// Delaying an error response gives the leader a chance to respond before us
 				s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil)
 			}
@@ -1684,7 +1634,7 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, subject, repl
 		// We have the stream assigned and a leader, so only the stream leader should answer.
 		if !acc.JetStreamIsStreamLeader(streamName) && !isLeaderless {
 			if js.isLeaderless() {
-				resp.Error = jsClusterNotAvailErr
+				resp.Error = ApiErrors[JSClusterNotAvailErr]
 				// Delaying an error response gives the leader a chance to respond before us
 				s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), sa.Group)
 			}
@@ -1694,7 +1644,7 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, subject, repl
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
@@ -1704,7 +1654,7 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, subject, repl
 	if !isEmptyRequest(msg) {
 		var req JSApiStreamInfoRequest
 		if err := json.Unmarshal(msg, &req); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -1713,7 +1663,7 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, subject, repl
 
 	mset, err := acc.lookupStream(streamName)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1759,7 +1709,7 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, sub
 
 	// If we are not in clustered mode this is a failed request.
 	if !s.JetStreamIsClustered() {
-		resp.Error = jsClusterRequiredErr
+		resp.Error = ApiErrors[JSClusterRequiredErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1770,7 +1720,7 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, sub
 		return
 	}
 	if js.isLeaderless() {
-		resp.Error = jsClusterNotAvailErr
+		resp.Error = ApiErrors[JSClusterNotAvailErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1780,7 +1730,7 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, sub
 	js.mu.RUnlock()
 
 	if isLeader && sa == nil {
-		resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+		resp.Error = ApiErrors[JSStreamNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	} else if sa == nil {
@@ -1789,20 +1739,20 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, sub
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
 	}
 	if !isEmptyRequest(msg) {
-		resp.Error = jsBadRequestErr
+		resp.Error = ApiErrors[JSBadRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	// Check to see if we are a member of the group and if the group has no leader.
 	if js.isGroupLeaderless(sa.Group) {
-		resp.Error = jsClusterNotAvailErr
+		resp.Error = ApiErrors[JSClusterNotAvailErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1814,7 +1764,7 @@ func (s *Server) jsStreamLeaderStepDownRequest(sub *subscription, c *client, sub
 
 	mset, err := acc.lookupStream(name)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1845,7 +1795,7 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, s
 
 	// If we are not in clustered mode this is a failed request.
 	if !s.JetStreamIsClustered() {
-		resp.Error = jsClusterRequiredErr
+		resp.Error = ApiErrors[JSClusterRequiredErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1856,7 +1806,7 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, s
 		return
 	}
 	if js.isLeaderless() {
-		resp.Error = jsClusterNotAvailErr
+		resp.Error = ApiErrors[JSClusterNotAvailErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1870,7 +1820,7 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, s
 	js.mu.RUnlock()
 
 	if isLeader && sa == nil {
-		resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+		resp.Error = ApiErrors[JSStreamNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	} else if sa == nil {
@@ -1881,13 +1831,13 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, s
 		ca = sa.consumers[consumer]
 	}
 	if ca == nil {
-		resp.Error = jsNoConsumerErr
+		resp.Error = ApiErrors[JSConsumerNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	// Check to see if we are a member of the group and if the group has no leader.
 	if js.isGroupLeaderless(ca.Group) {
-		resp.Error = jsClusterNotAvailErr
+		resp.Error = ApiErrors[JSClusterNotAvailErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1898,26 +1848,26 @@ func (s *Server) jsConsumerLeaderStepDownRequest(sub *subscription, c *client, s
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
 	}
 	if !isEmptyRequest(msg) {
-		resp.Error = jsBadRequestErr
+		resp.Error = ApiErrors[JSBadRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	mset, err := acc.lookupStream(stream)
 	if err != nil {
-		resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+		resp.Error = ApiErrors[JSStreamNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	o := mset.lookupConsumer(consumer)
 	if o == nil {
-		resp.Error = jsNoConsumerErr
+		resp.Error = ApiErrors[JSConsumerNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1947,7 +1897,7 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, subject
 
 	// If we are not in clustered mode this is a failed request.
 	if !s.JetStreamIsClustered() {
-		resp.Error = jsClusterRequiredErr
+		resp.Error = ApiErrors[JSClusterRequiredErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1958,7 +1908,7 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, subject
 		return
 	}
 	if js.isLeaderless() {
-		resp.Error = jsClusterNotAvailErr
+		resp.Error = ApiErrors[JSClusterNotAvailErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -1974,32 +1924,32 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, subject
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
 	}
 	if isEmptyRequest(msg) {
-		resp.Error = jsBadRequestErr
+		resp.Error = ApiErrors[JSBadRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	var req JSApiStreamRemovePeerRequest
 	if err := json.Unmarshal(msg, &req); err != nil {
-		resp.Error = jsInvalidJSONErr
+		resp.Error = ApiErrors[JSInvalidJSONErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	if req.Peer == _EMPTY_ {
-		resp.Error = jsBadRequestErr
+		resp.Error = ApiErrors[JSBadRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	if sa == nil {
 		// No stream present.
-		resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+		resp.Error = ApiErrors[JSStreamNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -2015,14 +1965,14 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, subject
 
 	// Make sure we are a member.
 	if !isMember {
-		resp.Error = jsPeerNotMemberErr
+		resp.Error = ApiErrors[JSClusterPeerNotMemberErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	// If we are here we have a valid peer member set for removal.
 	if !js.removePeerFromStream(sa, nodeName) {
-		resp.Error = jsPeerRemapErr
+		resp.Error = ApiErrors[JSPeerRemapErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -2060,14 +2010,14 @@ func (s *Server) jsLeaderServerRemoveRequest(sub *subscription, c *client, subje
 	var resp = JSApiMetaServerRemoveResponse{ApiResponse: ApiResponse{Type: JSApiMetaServerRemoveResponseType}}
 
 	if isEmptyRequest(msg) {
-		resp.Error = jsBadRequestErr
+		resp.Error = ApiErrors[JSBadRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	var req JSApiMetaServerRemoveRequest
 	if err := json.Unmarshal(msg, &req); err != nil {
-		resp.Error = jsInvalidJSONErr
+		resp.Error = ApiErrors[JSInvalidJSONErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -2084,7 +2034,7 @@ func (s *Server) jsLeaderServerRemoveRequest(sub *subscription, c *client, subje
 	js.mu.RUnlock()
 
 	if found == _EMPTY_ {
-		resp.Error = jsServerNotMemberErr
+		resp.Error = ApiErrors[JSClusterServerNotMemberErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -2131,13 +2081,13 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, subject,
 	if !isEmptyRequest(msg) {
 		var req JSApiLeaderStepdownRequest
 		if err := json.Unmarshal(msg, &req); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
 		if len(req.Placement.Tags) > 0 {
 			// Tags currently not supported.
-			resp.Error = jsClusterTagsErr
+			resp.Error = ApiErrors[JSClusterTagsErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -2153,7 +2103,7 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, subject,
 			}
 		}
 		if len(peers) == 0 {
-			resp.Error = jsClusterNoPeersErr
+			resp.Error = ApiErrors[JSClusterNoPeersErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -2166,9 +2116,8 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, subject,
 
 	// Call actual stepdown.
 	err = cc.meta.StepDown(preferredLeader)
-
 	if err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSRaftGeneralErrF].ErrOr(err)
 	} else {
 		resp.Success = true
 	}
@@ -2214,7 +2163,7 @@ func (s *Server) jsStreamDeleteRequest(sub *subscription, c *client, subject, re
 			return
 		}
 		if js.isLeaderless() {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -2226,14 +2175,14 @@ func (s *Server) jsStreamDeleteRequest(sub *subscription, c *client, subject, re
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
 	}
 
 	if !isEmptyRequest(msg) {
-		resp.Error = jsNotEmptyRequestErr
+		resp.Error = ApiErrors[JSNotEmptyRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -2247,13 +2196,13 @@ func (s *Server) jsStreamDeleteRequest(sub *subscription, c *client, subject, re
 
 	mset, err := acc.lookupStream(stream)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	if err := mset.delete(); err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSStreamDeleteErrF].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -2285,7 +2234,7 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, subject, reply
 			return
 		}
 		if js.isLeaderless() {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -2298,13 +2247,13 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, subject, reply
 			// We can't find the stream, so mimic what would be the errors below.
 			if hasJS, doErr := acc.checkJetStream(); !hasJS {
 				if doErr {
-					resp.Error = jsNotEnabledErr
+					resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 					s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 				}
 				return
 			}
 			// No stream present.
-			resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+			resp.Error = ApiErrors[JSStreamNotFoundErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		} else if sa == nil {
@@ -2313,7 +2262,7 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, subject, reply
 
 		// Check to see if we are a member of the group and if the group has no leader.
 		if js.isGroupLeaderless(sa.Group) {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -2326,26 +2275,26 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, subject, reply
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
 	}
 	if isEmptyRequest(msg) {
-		resp.Error = jsBadRequestErr
+		resp.Error = ApiErrors[JSBadRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	var req JSApiMsgDeleteRequest
 	if err := json.Unmarshal(msg, &req); err != nil {
-		resp.Error = jsInvalidJSONErr
+		resp.Error = ApiErrors[JSInvalidJSONErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	mset, err := acc.lookupStream(stream)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -2362,9 +2311,9 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, subject, reply
 		removed, err = mset.eraseMsg(req.Seq)
 	}
 	if err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSStreamMsgDeleteFailedF].ErrOr(err)
 	} else if !removed {
-		resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("sequence [%d] not found", req.Seq)}
+		resp.Error = ApiErrors[JSSequenceNotFoundErrF].NewT("{seq}", req.Seq)
 	} else {
 		resp.Success = true
 	}
@@ -2394,7 +2343,7 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, subject, reply st
 			return
 		}
 		if js.isLeaderless() {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -2407,13 +2356,13 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, subject, reply st
 			// We can't find the stream, so mimic what would be the errors below.
 			if hasJS, doErr := acc.checkJetStream(); !hasJS {
 				if doErr {
-					resp.Error = jsNotEnabledErr
+					resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 					s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 				}
 				return
 			}
 			// No stream present.
-			resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+			resp.Error = ApiErrors[JSStreamNotFoundErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		} else if sa == nil {
@@ -2422,7 +2371,7 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, subject, reply st
 
 		// Check to see if we are a member of the group and if the group has no leader.
 		if js.isGroupLeaderless(sa.Group) {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -2435,33 +2384,33 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, subject, reply st
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
 	}
 	if isEmptyRequest(msg) {
-		resp.Error = jsBadRequestErr
+		resp.Error = ApiErrors[JSBadRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	var req JSApiMsgGetRequest
 	if err := json.Unmarshal(msg, &req); err != nil {
-		resp.Error = jsInvalidJSONErr
+		resp.Error = ApiErrors[JSInvalidJSONErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	mset, err := acc.lookupStream(stream)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	subj, hdr, msg, ts, err := mset.store.LoadMsg(req.Seq)
 	if err != nil {
-		resp.Error = jsNoMessageFoundErr
+		resp.Error = ApiErrors[JSNoMessageFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -2506,18 +2455,18 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, rep
 			// We can't find the stream, so mimic what would be the errors below.
 			if hasJS, doErr := acc.checkJetStream(); !hasJS {
 				if doErr {
-					resp.Error = jsNotEnabledErr
+					resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 					s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 				}
 				return
 			}
 			// No stream present.
-			resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+			resp.Error = ApiErrors[JSStreamNotFoundErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		} else if sa == nil {
 			if js.isLeaderless() {
-				resp.Error = jsClusterNotAvailErr
+				resp.Error = ApiErrors[JSClusterNotAvailErr]
 				s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			}
 			return
@@ -2525,7 +2474,7 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, rep
 
 		// Check to see if we are a member of the group and if the group has no leader.
 		if js.isGroupLeaderless(sa.Group) {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -2533,7 +2482,7 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, rep
 		// We have the stream assigned and a leader, so only the stream leader should answer.
 		if !acc.JetStreamIsStreamLeader(stream) {
 			if js.isLeaderless() {
-				resp.Error = jsClusterNotAvailErr
+				resp.Error = ApiErrors[JSClusterNotAvailErr]
 				s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			}
 			return
@@ -2542,20 +2491,20 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, rep
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
 	}
 
 	if !isEmptyRequest(msg) {
-		resp.Error = jsNotEmptyRequestErr
+		resp.Error = ApiErrors[JSNotEmptyRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	mset, err := acc.lookupStream(stream)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -2567,7 +2516,7 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, subject, rep
 
 	purged, err := mset.purge()
 	if err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSStreamGeneralErrorF].ErrOr(err)
 	} else {
 		resp.Purged = purged
 		resp.Success = true
@@ -2588,19 +2537,19 @@ func (s *Server) jsStreamRestoreRequest(sub *subscription, c *client, subject, r
 
 	var resp = JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}}
 	if !acc.JetStreamEnabled() {
-		resp.Error = jsNotEnabledErr
+		resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	if isEmptyRequest(msg) {
-		resp.Error = jsBadRequestErr
+		resp.Error = ApiErrors[JSBadRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	var req JSApiStreamRestoreRequest
 	if err := json.Unmarshal(msg, &req); err != nil {
-		resp.Error = jsInvalidJSONErr
+		resp.Error = ApiErrors[JSInvalidJSONErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -2617,14 +2566,14 @@ func (s *Server) jsStreamRestoreRequest(sub *subscription, c *client, subject, r
 	}
 
 	if _, err := acc.lookupStream(stream); err == nil {
-		resp.Error = jsError(ErrJetStreamStreamAlreadyUsed)
+		resp.Error = ApiErrors[JSStreamNameExistErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
@@ -2649,8 +2598,8 @@ func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamC
 
 	tfile, err := ioutil.TempFile(snapDir, "js-restore-")
 	if err != nil {
-		resp.Error = &ApiError{Code: 500, Description: "JetStream unable to open temp storage for restore"}
-		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
+		resp.Error = ApiErrors[JSTempStorageFailedErr]
+		s.sendAPIErrResponse(ci, acc, subject, reply, msg, s.jsonResponse(&resp))
 		return nil
 	}
 
@@ -2719,7 +2668,7 @@ func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamC
 		total += len(msg)
 		if js.wouldExceedLimits(FileStorage, total) {
 			s.resourcesExeededError()
-			resultCh <- result{ErrJetStreamResourcesExceeded, reply}
+			resultCh <- result{ApiErrors[JSInsufficientResourcesErr], reply}
 			return
 		}
 
@@ -2741,14 +2690,14 @@ func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamC
 	if err != nil {
 		tfile.Close()
 		os.Remove(tfile.Name())
-		resp.Error = &ApiError{Code: 500, Description: "JetStream unable to subscribe to restore snapshot " + restoreSubj + ": " + err.Error()}
-		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
+		resp.Error = ApiErrors[JSRestoreSubscribeFailedErrF].NewT("{subject}", restoreSubj, "{err}", err)
+		s.sendAPIErrResponse(ci, acc, subject, reply, msg, s.jsonResponse(&resp))
 		return nil
 	}
 
 	// Mark the subject so the end user knows where to send the snapshot chunks.
 	resp.DeliverSubject = restoreSubj
-	s.sendAPIResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(resp))
+	s.sendAPIResponse(ci, acc, subject, reply, msg, s.jsonResponse(resp))
 
 	doneCh := make(chan error, 1)
 
@@ -2804,7 +2753,7 @@ func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamC
 				var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}}
 
 				if err != nil {
-					resp.Error = jsError(err)
+					resp.Error = ApiErrors[JSStreamRestoreErrF].ErrOrNewT(err, "{err}", err)
 					s.Warnf("Restore failed for %s for stream '%s > %s' in %v",
 						friendlyBytes(int64(total)), streamName, acc.Name, end.Sub(start))
 				} else {
@@ -2853,26 +2802,26 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, subject,
 
 	var resp = JSApiStreamSnapshotResponse{ApiResponse: ApiResponse{Type: JSApiStreamSnapshotResponseType}}
 	if !acc.JetStreamEnabled() {
-		resp.Error = jsNotEnabledErr
+		resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp))
 		return
 	}
 	if isEmptyRequest(msg) {
-		resp.Error = jsBadRequestErr
+		resp.Error = ApiErrors[JSBadRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp))
 		return
 	}
 
 	mset, err := acc.lookupStream(stream)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp))
 		return
 	}
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp))
 		}
 		return
@@ -2880,12 +2829,12 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, subject,
 
 	var req JSApiStreamSnapshotRequest
 	if err := json.Unmarshal(msg, &req); err != nil {
-		resp.Error = jsInvalidJSONErr
+		resp.Error = ApiErrors[JSInvalidJSONErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp))
 		return
 	}
 	if !IsValidSubject(req.DeliverSubject) {
-		resp.Error = &ApiError{Code: 400, Description: "deliver subject not valid"}
+		resp.Error = ApiErrors[JSSnapshotDeliverSubjectInvalidErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp))
 		return
 	}
@@ -2904,7 +2853,7 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, subject,
 		sr, err := mset.snapshot(0, req.CheckMsgs, !req.NoConsumers)
 		if err != nil {
 			s.Warnf("Snapshot of stream '%s > %s' failed: %v", mset.jsa.account.Name, mset.name(), err)
-			resp.Error = jsError(err)
+			resp.Error = ApiErrors[JSStreamSnapshotErrF].ErrOr(err)
 			s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp))
 			return
 		}
@@ -3069,7 +3018,7 @@ func (s *Server) jsConsumerCreate(sub *subscription, c *client, subject, reply s
 
 	var req CreateConsumerRequest
 	if err := json.Unmarshal(msg, &req); err != nil {
-		resp.Error = jsInvalidJSONErr
+		resp.Error = ApiErrors[JSInvalidJSONErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -3087,7 +3036,7 @@ func (s *Server) jsConsumerCreate(sub *subscription, c *client, subject, reply s
 				return
 			}
 			if js.isLeaderless() {
-				resp.Error = jsClusterNotAvailErr
+				resp.Error = ApiErrors[JSClusterNotAvailErr]
 				s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 				return
 			}
@@ -3100,43 +3049,43 @@ func (s *Server) jsConsumerCreate(sub *subscription, c *client, subject, reply s
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
 	}
 	if streamName != req.Stream {
-		resp.Error = jsStreamMismatchErr
+		resp.Error = ApiErrors[JSStreamMismatchErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	if expectDurable {
 		if numTokens(subject) != 7 {
-			resp.Error = &ApiError{Code: 400, Description: "consumer expected to be durable but no durable name set in subject"}
+			resp.Error = ApiErrors[JSConsumerDurableNameNotInSubjectErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
 		// Now check on requirements for durable request.
 		if req.Config.Durable == _EMPTY_ {
-			resp.Error = &ApiError{Code: 400, Description: "consumer expected to be durable but a durable name was not set"}
+			resp.Error = ApiErrors[JSConsumerDurableNameNotSetErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
 		consumerName := tokenAt(subject, 7)
 		if consumerName != req.Config.Durable {
-			resp.Error = &ApiError{Code: 400, Description: "consumer name in subject does not match durable name in request"}
+			resp.Error = ApiErrors[JSConsumerDurableNameNotMatchSubjectErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
 	} else {
 		if numTokens(subject) != 5 {
-			resp.Error = &ApiError{Code: 400, Description: "consumer expected to be ephemeral but detected a durable name set in subject"}
+			resp.Error = ApiErrors[JSConsumerEphemeralWithDurableInSubjectErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
 		if req.Config.Durable != _EMPTY_ {
-			resp.Error = &ApiError{Code: 400, Description: "consumer expected to be ephemeral but a durable name was set in request"}
+			resp.Error = ApiErrors[JSConsumerEphemeralWithDurableNameErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -3149,14 +3098,14 @@ func (s *Server) jsConsumerCreate(sub *subscription, c *client, subject, reply s
 
 	stream, err := acc.lookupStream(req.Stream)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	o, err := stream.addConsumer(&req.Config)
 	if err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSConsumerCreateErrF].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -3187,7 +3136,7 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, subject, r
 			return
 		}
 		if js.isLeaderless() {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -3199,7 +3148,7 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, subject, r
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
@@ -3209,7 +3158,7 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, subject, r
 	if !isEmptyRequest(msg) {
 		var req JSApiConsumersRequest
 		if err := json.Unmarshal(msg, &req); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -3229,14 +3178,14 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, subject, r
 		sas := cc.streams[acc.Name]
 		if sas == nil {
 			js.mu.RUnlock()
-			resp.Error = jsNotFoundError(ErrJetStreamNotEnabled)
+			resp.Error = ApiErrors[JSStreamNotFoundErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
 		sa := sas[streamName]
 		if sa == nil || sa.err != nil {
 			js.mu.RUnlock()
-			resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+			resp.Error = ApiErrors[JSStreamNotFoundErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -3256,7 +3205,7 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, subject, r
 	} else {
 		mset, err := acc.lookupStream(streamName)
 		if err != nil {
-			resp.Error = jsNotFoundError(err)
+			resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOr(err)
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -3308,7 +3257,7 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, subject, re
 			return
 		}
 		if js.isLeaderless() {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -3320,7 +3269,7 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, subject, re
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
@@ -3330,7 +3279,7 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, subject, re
 	if !isEmptyRequest(msg) {
 		var req JSApiConsumersRequest
 		if err := json.Unmarshal(msg, &req); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -3350,7 +3299,7 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, subject, re
 
 	mset, err := acc.lookupStream(streamName)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -3413,23 +3362,23 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, subject, re
 			// We can't find the consumer, so mimic what would be the errors below.
 			if hasJS, doErr := acc.checkJetStream(); !hasJS {
 				if doErr {
-					resp.Error = jsNotEnabledErr
+					resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 					s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 				}
 				return
 			}
 			if sa == nil {
-				resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+				resp.Error = ApiErrors[JSStreamNotFoundErr]
 				s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 				return
 			}
 			// If we are here the consumer is not present.
-			resp.Error = jsNoConsumerErr
+			resp.Error = ApiErrors[JSConsumerNotFoundErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		} else if ca == nil {
 			if js.isLeaderless() {
-				resp.Error = jsClusterNotAvailErr
+				resp.Error = ApiErrors[JSClusterNotAvailErr]
 				// Delaying an error response gives the leader a chance to respond before us
 				s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil)
 			}
@@ -3438,7 +3387,7 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, subject, re
 
 		// Check to see if we are a member of the group and if the group has no leader.
 		if js.isGroupLeaderless(ca.Group) {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -3446,13 +3395,13 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, subject, re
 		// We have the consumer assigned and a leader, so only the consumer leader should answer.
 		if !acc.JetStreamIsConsumerLeader(streamName, consumerName) {
 			if js.isLeaderless() {
-				resp.Error = jsClusterNotAvailErr
+				resp.Error = ApiErrors[JSClusterNotAvailErr]
 				// Delaying an error response gives the leader a chance to respond before us
 				s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), ca.Group)
 			} else if rg := ca.Group; rg != nil && rg.node != nil && rg.isMember(cc.meta.ID()) {
 				// Check here if we are a member and this is just a new consumer that does not have a leader yet.
 				if rg.node.GroupLeader() == _EMPTY_ && !rg.node.HadPreviousLeader() {
-					resp.Error = jsNoConsumerErr
+					resp.Error = ApiErrors[JSConsumerNotFoundErr]
 					s.sendDelayedAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp), nil)
 				}
 			}
@@ -3461,26 +3410,26 @@ func (s *Server) jsConsumerInfoRequest(sub *subscription, c *client, subject, re
 	}
 
 	if !acc.JetStreamEnabled() {
-		resp.Error = jsNotEnabledErr
+		resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	if !isEmptyRequest(msg) {
-		resp.Error = jsNotEmptyRequestErr
+		resp.Error = ApiErrors[JSNotEmptyRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	mset, err := acc.lookupStream(streamName)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	obs := mset.lookupConsumer(consumerName)
 	if obs == nil {
-		resp.Error = jsNoConsumerErr
+		resp.Error = ApiErrors[JSConsumerNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -3508,7 +3457,7 @@ func (s *Server) jsConsumerDeleteRequest(sub *subscription, c *client, subject,
 			return
 		}
 		if js.isLeaderless() {
-			resp.Error = jsClusterNotAvailErr
+			resp.Error = ApiErrors[JSClusterNotAvailErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 			return
 		}
@@ -3520,13 +3469,13 @@ func (s *Server) jsConsumerDeleteRequest(sub *subscription, c *client, subject,
 
 	if hasJS, doErr := acc.checkJetStream(); !hasJS {
 		if doErr {
-			resp.Error = jsNotEnabledErr
+			resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		}
 		return
 	}
 	if !isEmptyRequest(msg) {
-		resp.Error = jsNotEmptyRequestErr
+		resp.Error = ApiErrors[JSNotEmptyRequestErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
@@ -3540,19 +3489,19 @@ func (s *Server) jsConsumerDeleteRequest(sub *subscription, c *client, subject,
 
 	mset, err := acc.lookupStream(stream)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 
 	obs := mset.lookupConsumer(consumer)
 	if obs == nil {
-		resp.Error = jsNoConsumerErr
+		resp.Error = ApiErrors[JSConsumerNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
 	if err := obs.delete(); err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSStreamGeneralErrorF].ErrOr(err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
 		return
 	}
diff --git a/server/jetstream_cluster.go b/server/jetstream_cluster.go
index 1f715187..f985625e 100644
--- a/server/jetstream_cluster.go
+++ b/server/jetstream_cluster.go
@@ -204,7 +204,7 @@ func (s *Server) JetStreamIsCurrent() bool {
 func (s *Server) JetStreamSnapshotMeta() error {
 	js := s.getJetStream()
 	if js == nil {
-		return ErrJetStreamNotEnabled
+		return ApiErrors[JSNotEnabledErr]
 	}
 	js.mu.RLock()
 	cc := js.cluster
@@ -222,10 +222,10 @@ func (s *Server) JetStreamSnapshotMeta() error {
 func (s *Server) JetStreamStepdownStream(account, stream string) error {
 	js, cc := s.getJetStreamCluster()
 	if js == nil {
-		return ErrJetStreamNotEnabled
+		return ApiErrors[JSNotEnabledErr]
 	}
 	if cc == nil {
-		return ErrJetStreamNotClustered
+		return ApiErrors[JSClusterNotActiveErr]
 	}
 	// Grab account
 	acc, err := s.LookupAccount(account)
@@ -248,10 +248,10 @@ func (s *Server) JetStreamStepdownStream(account, stream string) error {
 func (s *Server) JetStreamSnapshotStream(account, stream string) error {
 	js, cc := s.getJetStreamCluster()
 	if js == nil {
-		return ErrJetStreamNotEnabled
+		return ApiErrors[JSNotEnabledForAccountErr]
 	}
 	if cc == nil {
-		return ErrJetStreamNotClustered
+		return ApiErrors[JSClusterNotActiveErr]
 	}
 	// Grab account
 	acc, err := s.LookupAccount(account)
@@ -267,7 +267,7 @@ func (s *Server) JetStreamSnapshotStream(account, stream string) error {
 	mset.mu.RLock()
 	if !mset.node.Leader() {
 		mset.mu.RUnlock()
-		return ErrJetStreamNotLeader
+		return ApiErrors[JSNotEnabledForAccountErr]
 	}
 	n := mset.node
 	mset.mu.RUnlock()
@@ -435,7 +435,7 @@ func (s *Server) enableJetStreamClustering() error {
 	}
 	js := s.getJetStream()
 	if js == nil {
-		return ErrJetStreamNotEnabled
+		return ApiErrors[JSNotEnabledForAccountErr]
 	}
 	// Already set.
 	if js.cluster != nil {
@@ -1191,7 +1191,7 @@ func (js *jetStream) createRaftGroup(rg *raftGroup, storage StorageType) error {
 	s, cc := js.srv, js.cluster
 
 	if cc == nil || cc.meta == nil {
-		return ErrJetStreamNotClustered
+		return ApiErrors[JSClusterNotActiveErr]
 	}
 
 	// If this is a single peer raft group or we are not a member return.
@@ -1435,7 +1435,7 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment) {
 					Stream:  sa.Config.Name,
 					Restore: &JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}},
 				}
-				result.Restore.Error = jsError(sa.err)
+				result.Restore.Error = ApiErrors[JSStreamAssignmentErrF].ErrOrNewT(sa.err, "{err}", sa.err)
 				js.mu.Unlock()
 				// Send response to the metadata leader. They will forward to the user as needed.
 				s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, result)
@@ -1630,10 +1630,10 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco
 				if isLeader && !isRecovering {
 					var resp = JSApiMsgDeleteResponse{ApiResponse: ApiResponse{Type: JSApiMsgDeleteResponseType}}
 					if err != nil {
-						resp.Error = jsError(err)
+						resp.Error = ApiErrors[JSStreamMsgDeleteFailedF].ErrOrNewT(err, "{err}", err)
 						s.sendAPIErrResponse(md.Client, mset.account(), md.Subject, md.Reply, _EMPTY_, s.jsonResponse(resp))
 					} else if !removed {
-						resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("sequence [%d] not found", md.Seq)}
+						resp.Error = ApiErrors[JSSequenceNotFoundErrF].NewT("{seq}", md.Seq)
 						s.sendAPIErrResponse(md.Client, mset.account(), md.Subject, md.Reply, _EMPTY_, s.jsonResponse(resp))
 					} else {
 						resp.Success = true
@@ -1667,7 +1667,7 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco
 				if isLeader && !isRecovering {
 					var resp = JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}}
 					if err != nil {
-						resp.Error = jsError(err)
+						resp.Error = ApiErrors[JSStreamGeneralErrorF].ErrOrNewT(err, "{err}", err)
 						s.sendAPIErrResponse(sp.Client, mset.account(), sp.Subject, sp.Reply, _EMPTY_, s.jsonResponse(resp))
 					} else {
 						resp.Purged = purged
@@ -1779,7 +1779,7 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) {
 	// Send our response.
 	var resp = JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}}
 	if err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSStreamCreateErrF].ErrOrNewT(err, "{err}", err)
 		s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp))
 	} else {
 		resp.StreamInfo = &StreamInfo{
@@ -1920,7 +1920,7 @@ func (js *jetStream) processStreamAssignment(sa *streamAssignment) bool {
 				Stream:   stream,
 				Response: &JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}},
 			}
-			result.Response.Error = jsNoAccountErr
+			result.Response.Error = ApiErrors[JSNoAccountErr]
 			s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, result)
 			s.Warnf(ll)
 		} else {
@@ -2071,7 +2071,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, sa *streamAssignme
 			Response: &JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}},
 			Update:   true,
 		}
-		result.Response.Error = jsError(err)
+		result.Response.Error = ApiErrors[JSStreamGeneralErrorF].ErrOrNewT(err, "{err}", err)
 		js.mu.Unlock()
 
 		// Send response to the metadata leader. They will forward to the user as needed.
@@ -2141,7 +2141,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
 				s.Warnf("JetStream cluster error updating stream %q for account %q: %v", sa.Config.Name, acc.Name, err)
 				mset.setStreamAssignment(osa)
 			}
-		} else if err == ErrJetStreamStreamNotFound {
+		} else if err == ApiErrors[JSStreamNotFoundErr] {
 			// Add in the stream here.
 			mset, err = acc.addStreamWithAssignment(sa.Config, nil, sa)
 		}
@@ -2174,7 +2174,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
 				Stream:   sa.Config.Name,
 				Response: &JSApiStreamCreateResponse{ApiResponse: ApiResponse{Type: JSApiStreamCreateResponseType}},
 			}
-			result.Response.Error = jsError(err)
+			result.Response.Error = ApiErrors[JSStreamCreateErrF].ErrOrNewT(err, "{err}", err)
 		}
 		js.mu.Unlock()
 
@@ -2218,7 +2218,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
 							Stream:  sa.Config.Name,
 							Restore: &JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}},
 						}
-						result.Restore.Error = jsError(sa.err)
+						result.Restore.Error = ApiErrors[JSStreamRestoreErrF].ErrOrNewT(sa.err, "{err}", sa.err)
 						js.mu.Unlock()
 						// Send response to the metadata leader. They will forward to the user as needed.
 						b, _ := json.Marshal(result) // Avoids auto-processing and doing fancy json with newlines.
@@ -2331,7 +2331,7 @@ func (js *jetStream) processClusterDeleteStream(sa *streamAssignment, isMember,
 	// Go ahead and delete the stream.
 	mset, err := acc.lookupStream(sa.Config.Name)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNewT(err, "{err}", err)
 	} else if mset != nil {
 		err = mset.stop(true, wasLeader)
 	}
@@ -2346,7 +2346,7 @@ func (js *jetStream) processClusterDeleteStream(sa *streamAssignment, isMember,
 
 	if err != nil {
 		if resp.Error == nil {
-			resp.Error = jsError(err)
+			resp.Error = ApiErrors[JSStreamGeneralErrorF].ErrOrNewT(err, "{err}", err)
 		}
 		s.sendAPIErrResponse(sa.Client, acc, sa.Subject, sa.Reply, _EMPTY_, s.jsonResponse(resp))
 	} else {
@@ -2386,7 +2386,7 @@ func (js *jetStream) processConsumerAssignment(ca *consumerAssignment) {
 				Consumer: consumer,
 				Response: &JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}},
 			}
-			result.Response.Error = jsNoAccountErr
+			result.Response.Error = ApiErrors[JSNoAccountErr]
 			s.sendInternalMsgLocked(consumerAssignmentSubj, _EMPTY_, nil, result)
 			s.Warnf(ll)
 		} else {
@@ -2492,14 +2492,14 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state
 	if err != nil {
 		js.mu.Lock()
 		s.Debugf("Consumer create failed, could not locate stream '%s > %s'", ca.Client.serviceAccount(), ca.Stream)
-		ca.err = ErrJetStreamStreamNotFound
+		ca.err = ApiErrors[JSStreamNotFoundErr]
 		result := &consumerAssignmentResult{
 			Account:  ca.Client.serviceAccount(),
 			Stream:   ca.Stream,
 			Consumer: ca.Name,
 			Response: &JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}},
 		}
-		result.Response.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+		result.Response.Error = ApiErrors[JSStreamNotFoundErr]
 		s.sendInternalMsgLocked(consumerAssignmentSubj, _EMPTY_, nil, result)
 		js.mu.Unlock()
 		return
@@ -2524,7 +2524,7 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state
 					Consumer: ca.Name,
 					Response: &JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}},
 				}
-				result.Response.Error = jsNotFoundError(ErrJetStreamConsumerAlreadyUsed)
+				result.Response.Error = ApiErrors[JSConsumerNameExistErr]
 				s.sendInternalMsgLocked(consumerAssignmentSubj, _EMPTY_, nil, result)
 				js.mu.Unlock()
 				return
@@ -2568,7 +2568,7 @@ func (js *jetStream) processClusterCreateConsumer(ca *consumerAssignment, state
 				Consumer: ca.Name,
 				Response: &JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}},
 			}
-			result.Response.Error = jsError(err)
+			result.Response.Error = ApiErrors[JSConsumerCreateErrF].ErrOrNewT(err, "{err}", err)
 		} else if err == errNoInterest {
 			// This is a stranded ephemeral, let's clean this one up.
 			subject := fmt.Sprintf(JSApiConsumerDeleteT, ca.Stream, ca.Name)
@@ -2614,12 +2614,12 @@ func (js *jetStream) processClusterDeleteConsumer(ca *consumerAssignment, isMemb
 	// Go ahead and delete the consumer.
 	mset, err := acc.lookupStream(ca.Stream)
 	if err != nil {
-		resp.Error = jsNotFoundError(err)
+		resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNewT(err, "{err}", err)
 	} else if mset != nil {
 		if o := mset.lookupConsumer(ca.Name); o != nil {
 			err = o.stopWithFlags(true, true, wasLeader)
 		} else {
-			resp.Error = jsNoConsumerErr
+			resp.Error = ApiErrors[JSConsumerNotFoundErr]
 		}
 	}
 
@@ -2633,7 +2633,7 @@ func (js *jetStream) processClusterDeleteConsumer(ca *consumerAssignment, isMemb
 
 	if err != nil {
 		if resp.Error == nil {
-			resp.Error = jsError(err)
+			resp.Error = ApiErrors[JSStreamNotFoundErr].ErrOrNewT(err, "{err}", err)
 		}
 		s.sendAPIErrResponse(ca.Client, acc, ca.Subject, ca.Reply, _EMPTY_, s.jsonResponse(resp))
 	} else {
@@ -2968,7 +2968,7 @@ func (js *jetStream) processConsumerLeaderChange(o *consumer, isLeader bool) {
 
 	var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}}
 	if err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSConsumerCreateErrF].ErrOrNewT(err, "{err}", err)
 		s.sendAPIErrResponse(client, acc, subject, reply, _EMPTY_, s.jsonResponse(&resp))
 	} else {
 		resp.ConsumerInfo = o.info()
@@ -3102,7 +3102,7 @@ func (js *jetStream) processStreamAssignmentResults(sub *subscription, c *client
 		// TODO(dlc) - Could have mixed results, should track per peer.
 		// Set sa.err while we are deleting so we will not respond to list/names requests.
 		if !result.Update && time.Since(sa.Created) < 5*time.Second {
-			sa.err = ErrJetStreamNotAssigned
+			sa.err = ApiErrors[JSClusterNotAssignedErr]
 			cc.meta.Propose(encodeDeleteStreamAssignment(sa))
 		}
 	}
@@ -3133,7 +3133,7 @@ func (js *jetStream) processConsumerAssignmentResults(sub *subscription, c *clie
 			// TODO(dlc) - Could have mixed results, should track per peer.
 			if result.Response.Error != nil {
 				// So while we are deleting we will not respond to list/names requests.
-				ca.err = ErrJetStreamNotAssigned
+				ca.err = ApiErrors[JSClusterNotAssignedErr]
 				cc.meta.Propose(encodeDeleteConsumerAssignment(ca))
 			}
 		}
@@ -3319,14 +3319,14 @@ func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject,
 	acc.mu.RUnlock()
 
 	if jsa == nil {
-		resp.Error = jsNotEnabledErr
+		resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
 
 	ccfg, err := checkStreamCfg(config)
 	if err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSStreamInvalidConfigF].ErrOrNewT(err, "{err}", err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
@@ -3343,14 +3343,14 @@ func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject,
 	jsa.mu.RUnlock()
 
 	if exceeded {
-		resp.Error = jsError(fmt.Errorf("maximum number of streams reached"))
+		resp.Error = ApiErrors[JSMaximumStreamsLimitErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
 
 	// Check for stream limits here before proposing.
 	if err := jsa.checkLimits(cfg); err != nil {
-		resp.Error = jsError(err)
+		resp.Error = ApiErrors[JSStreamLimitsErrF].ErrOrNewT(err, "{err}", err)
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
@@ -3360,7 +3360,7 @@ func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject,
 	defer js.mu.Unlock()
 
 	if sa := js.streamAssignment(acc.Name, cfg.Name); sa != nil {
-		resp.Error = jsError(ErrJetStreamStreamAlreadyUsed)
+		resp.Error = ApiErrors[JSStreamNameExistErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
@@ -3370,7 +3370,7 @@ func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject,
 		for _, subj := range sa.Config.Subjects {
 			for _, tsubj := range cfg.Subjects {
 				if SubjectsCollide(tsubj, subj) {
-					resp.Error = jsError(fmt.Errorf("subjects overlap with an existing stream"))
+					resp.Error = ApiErrors[JSStreamSubjectOverlapErr]
 					s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 					return
 				}
@@ -3381,7 +3381,7 @@ func (s *Server) jsClusteredStreamRequest(ci *ClientInfo, acc *Account, subject,
 	// Raft group selection and placement.
 	rg := cc.createGroupForStream(ci, cfg)
 	if rg == nil {
-		resp.Error = jsInsufficientErr
+		resp.Error = ApiErrors[JSInsufficientResourcesErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
@@ -3407,32 +3407,32 @@ func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, su
 	osa := js.streamAssignment(acc.Name, cfg.Name)
 
 	if osa == nil {
-		resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+		resp.Error = ApiErrors[JSStreamNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
 	var newCfg *StreamConfig
 	if jsa := js.accounts[acc.Name]; jsa != nil {
 		if ncfg, err := jsa.configUpdateCheck(osa.Config, cfg); err != nil {
-			resp.Error = jsError(err)
+			resp.Error = ApiErrors[JSStreamUpdateErrF].ErrOrNewT(err, "{err}", err)
 			s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 			return
 		} else {
 			newCfg = ncfg
 		}
 	} else {
-		resp.Error = jsNotEnabledErr
+		resp.Error = ApiErrors[JSNotEnabledForAccountErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
 	// Check for cluster changes that we want to error on.
 	if newCfg.Replicas != len(osa.Group.Peers) {
-		resp.Error = &ApiError{Code: 400, Description: "Replicas configuration can not be updated"}
+		resp.Error = ApiErrors[JSStreamReplicasNotUpdatableErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
 	if !reflect.DeepEqual(newCfg.Mirror, osa.Config.Mirror) {
-		resp.Error = &ApiError{Code: 400, Description: "Mirror configuration can not be updated"}
+		resp.Error = ApiErrors[JSStreamMirrorNotUpdatableErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
@@ -3445,7 +3445,7 @@ func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, su
 		for _, subj := range sa.Config.Subjects {
 			for _, tsubj := range newCfg.Subjects {
 				if SubjectsCollide(tsubj, subj) {
-					resp.Error = jsError(fmt.Errorf("subjects overlap with an existing stream"))
+					resp.Error = ApiErrors[JSStreamSubjectOverlapErr]
 					s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 					return
 				}
@@ -3469,7 +3469,7 @@ func (s *Server) jsClusteredStreamDeleteRequest(ci *ClientInfo, acc *Account, st
 	osa := js.streamAssignment(acc.Name, stream)
 	if osa == nil {
 		var resp = JSApiStreamDeleteResponse{ApiResponse: ApiResponse{Type: JSApiStreamDeleteResponseType}}
-		resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+		resp.Error = ApiErrors[JSStreamNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
@@ -3495,7 +3495,7 @@ func (s *Server) jsClusteredStreamPurgeRequest(ci *ClientInfo, acc *Account, mse
 	sa := js.streamAssignment(acc.Name, stream)
 	if sa == nil {
 		resp := JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}}
-		resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+		resp.Error = ApiErrors[JSStreamNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
@@ -3507,7 +3507,7 @@ func (s *Server) jsClusteredStreamPurgeRequest(ci *ClientInfo, acc *Account, mse
 		var resp = JSApiStreamPurgeResponse{ApiResponse: ApiResponse{Type: JSApiStreamPurgeResponseType}}
 		purged, err := mset.purge()
 		if err != nil {
-			resp.Error = jsError(err)
+			resp.Error = ApiErrors[JSStreamGeneralErrorF].ErrOrNewT(err, "{err}", err)
 		} else {
 			resp.Purged = purged
 			resp.Success = true
@@ -3529,7 +3529,7 @@ func (s *Server) jsClusteredStreamRestoreRequest(ci *ClientInfo, acc *Account, r
 	resp := JSApiStreamRestoreResponse{ApiResponse: ApiResponse{Type: JSApiStreamRestoreResponseType}}
 
 	if sa := js.streamAssignment(ci.serviceAccount(), cfg.Name); sa != nil {
-		resp.Error = jsError(ErrJetStreamStreamAlreadyUsed)
+		resp.Error = ApiErrors[JSStreamNameExistErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
@@ -3537,7 +3537,7 @@ func (s *Server) jsClusteredStreamRestoreRequest(ci *ClientInfo, acc *Account, r
 	// Raft group selection and placement.
 	rg := cc.createGroupForStream(ci, cfg)
 	if rg == nil {
-		resp.Error = jsInsufficientErr
+		resp.Error = ApiErrors[JSInsufficientResourcesErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
@@ -3665,7 +3665,7 @@ LOOP:
 			return
 		case <-notActive.C:
 			s.Warnf("Did not receive all stream info results for %q", acc)
-			resp.Error = jsClusterIncompleteErr
+			resp.Error = ApiErrors[JSClusterIncompleteErr]
 			break LOOP
 		case si := <-rc:
 			resp.Streams = append(resp.Streams, si)
@@ -3843,19 +3843,19 @@ func (s *Server) jsClusteredConsumerDeleteRequest(ci *ClientInfo, acc *Account,
 
 	sa := js.streamAssignment(acc.Name, stream)
 	if sa == nil {
-		resp.Error = jsNotFoundError(ErrJetStreamStreamNotFound)
+		resp.Error = ApiErrors[JSStreamNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 
 	}
 	if sa.consumers == nil {
-		resp.Error = jsNoConsumerErr
+		resp.Error = ApiErrors[JSConsumerNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
 	oca := sa.consumers[consumer]
 	if oca == nil {
-		resp.Error = jsNoConsumerErr
+		resp.Error = ApiErrors[JSConsumerNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
@@ -3905,9 +3905,9 @@ func (s *Server) jsClusteredMsgDeleteRequest(ci *ClientInfo, acc *Account, mset
 		}
 		var resp = JSApiMsgDeleteResponse{ApiResponse: ApiResponse{Type: JSApiMsgDeleteResponseType}}
 		if err != nil {
-			resp.Error = jsError(err)
+			resp.Error = ApiErrors[JSStreamMsgDeleteFailedF].ErrOrNewT(err, "{err}", err)
 		} else if !removed {
-			resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("sequence [%d] not found", req.Seq)}
+			resp.Error = ApiErrors[JSSequenceNotFoundErrF].NewT("{seq}", req.Seq)
 		} else {
 			resp.Success = true
 		}
@@ -3966,7 +3966,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
 	// Lookup the stream assignment.
 	sa := js.streamAssignment(acc.Name, stream)
 	if sa == nil {
-		resp.Error = jsError(ErrJetStreamStreamNotFound)
+		resp.Error = ApiErrors[JSStreamNotFoundErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
@@ -3986,7 +3986,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
 
 	rg := cc.createGroupForConsumer(sa)
 	if rg == nil {
-		resp.Error = jsInsufficientErr
+		resp.Error = ApiErrors[JSInsufficientResourcesErr]
 		s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 		return
 	}
@@ -4021,7 +4021,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
 				shouldErr = len(rr.psubs)+len(rr.qsubs) != 0
 			}
 			if shouldErr {
-				resp.Error = jsError(ErrJetStreamConsumerAlreadyUsed)
+				resp.Error = ApiErrors[JSConsumerNameExistErr]
 				s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
 				return
 			}
@@ -4215,14 +4215,14 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [
 	if js.limitsExceeded(stype) {
 		s.resourcesExeededError()
 		if canRespond {
-			b, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: jsInsufficientErr})
+			b, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: ApiErrors[JSInsufficientResourcesErr]})
 			outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil})
 		}
 		// Stepdown regardless.
 		if node := mset.raftNode(); node != nil {
 			node.StepDown()
 		}
-		return 0, ErrJetStreamResourcesExceeded
+		return 0, ApiErrors[JSInsufficientResourcesErr]
 	}
 
 	// Check here pre-emptively if we have exceeded our account limits.
@@ -4247,7 +4247,7 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [
 		s.Warnf(err.Error())
 		if canRespond {
 			var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}}
-			resp.Error = &ApiError{Code: 400, Description: "resource limits exceeded for account"}
+			resp.Error = ApiErrors[JSAccountResourcesExceededErr]
 			response, _ = json.Marshal(resp)
 			outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, response, nil, 0, nil})
 		}
@@ -4260,7 +4260,7 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [
 		s.Warnf(err.Error())
 		if canRespond {
 			var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}}
-			resp.Error = &ApiError{Code: 400, Description: "message size exceeds maximum allowed"}
+			resp.Error = ApiErrors[JSStreamMessageExceedsMaximumErr]
 			response, _ = json.Marshal(resp)
 			outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, response, nil, 0, nil})
 		}
@@ -4533,7 +4533,7 @@ RETRY:
 			} else if isOutOfSpaceErr(err) {
 				s.handleOutOfSpace(msetName)
 				return
-			} else if err == ErrJetStreamResourcesExceeded {
+			} else if err == ApiErrors[JSInsufficientResourcesErr] {
 				s.resourcesExeededError()
 				return
 			} else {
@@ -4569,7 +4569,7 @@ func (mset *stream) processCatchupMsg(msg []byte) (uint64, error) {
 	}
 
 	if mset.js.limitsExceeded(mset.cfg.Storage) {
-		return 0, ErrJetStreamResourcesExceeded
+		return 0, ApiErrors[JSInsufficientResourcesErr]
 	}
 
 	// Put into our store
diff --git a/server/jetstream_errors.go b/server/jetstream_errors.go
new file mode 100644
index 00000000..02439891
--- /dev/null
+++ b/server/jetstream_errors.go
@@ -0,0 +1,105 @@
+package server
+
+import (
+	"fmt"
+	"strings"
+)
+
+type ErrorIdentifier uint16
+
+// IsNatsErr determines if a error matches ID, if multiple IDs are given if the error matches any of these the function will be true
+func IsNatsErr(err error, ids ...ErrorIdentifier) bool {
+	if err == nil {
+		return false
+	}
+
+	ce, ok := err.(*ApiError)
+	if !ok || ce == nil {
+		return false
+	}
+
+	found := false
+	for _, id := range ids {
+		ae, ok := ApiErrors[id]
+		if !ok || ae == nil {
+			continue
+		}
+
+		if ce.ErrCode == ae.ErrCode {
+			found = true
+		}
+	}
+
+	return found
+}
+
+// ApiError is included in all responses if there was an error.
+type ApiError struct {
+	Code        int    `json:"code"`
+	ErrCode     uint16 `json:"err_code,omitempty"`
+	Description string `json:"description,omitempty"`
+	URL         string `json:"-"`
+	Help        string `json:"-"`
+}
+
+func (e *ApiError) Error() string {
+	return fmt.Sprintf("%s (%d)", e.Description, e.ErrCode)
+}
+
+// ErrOrNewT returns err if it's an ApiError else creates a new error using NewT()
+func (e *ApiError) ErrOrNewT(err error, replacements ...interface{}) *ApiError {
+	if ae, ok := err.(*ApiError); ok {
+		return ae
+	}
+
+	return e.NewT(replacements...)
+}
+
+// ErrOr returns err if it's an ApiError else creates a new error
+func (e *ApiError) ErrOr(err error) *ApiError {
+	if ae, ok := err.(*ApiError); ok {
+		return ae
+	}
+
+	return e
+}
+
+// NewT creates a new error using strings.Replacer on the Description field, arguments must be an even number like NewT("{err}", err)
+func (e *ApiError) NewT(replacements ...interface{}) *ApiError {
+	ne := &ApiError{
+		Code:        e.Code,
+		ErrCode:     e.ErrCode,
+		Description: e.Description,
+	}
+
+	if len(replacements) == 0 {
+		return ne
+	}
+
+	if len(replacements)%2 != 0 {
+		panic("invalid error replacement")
+	}
+
+	var ra []string
+
+	var key string
+	for i, replacement := range replacements {
+		if i%2 == 0 {
+			key = replacement.(string)
+			continue
+		}
+
+		switch v := replacement.(type) {
+		case string:
+			ra = append(ra, key, v)
+		case error:
+			ra = append(ra, key, v.Error())
+		default:
+			ra = append(ra, key, fmt.Sprintf("%v", v))
+		}
+	}
+
+	ne.Description = strings.NewReplacer(ra...).Replace(e.Description)
+
+	return ne
+}
diff --git a/server/jetstream_errors_generated.go b/server/jetstream_errors_generated.go
new file mode 100644
index 00000000..a3be3d57
--- /dev/null
+++ b/server/jetstream_errors_generated.go
@@ -0,0 +1,332 @@
+// Generated code, do not edit. See errors.json and run go generate to update
+
+package server
+
+const (
+	// JSAccountResourcesExceededErr resource limits exceeded for account
+	JSAccountResourcesExceededErr ErrorIdentifier = 10002
+
+	// JSBadRequestErr bad request
+	JSBadRequestErr ErrorIdentifier = 10003
+
+	// JSClusterIncompleteErr incomplete results
+	JSClusterIncompleteErr ErrorIdentifier = 10004
+
+	// JSClusterNoPeersErr no suitable peers for placement
+	JSClusterNoPeersErr ErrorIdentifier = 10005
+
+	// JSClusterNotActiveErr JetStream not in clustered mode
+	JSClusterNotActiveErr ErrorIdentifier = 10006
+
+	// JSClusterNotAssignedErr JetStream cluster not assigned to this server
+	JSClusterNotAssignedErr ErrorIdentifier = 10007
+
+	// JSClusterNotAvailErr JetStream system temporarily unavailable
+	JSClusterNotAvailErr ErrorIdentifier = 10008
+
+	// JSClusterNotLeaderErr JetStream cluster can not handle request
+	JSClusterNotLeaderErr ErrorIdentifier = 10009
+
+	// JSClusterPeerNotMemberErr peer not a member
+	JSClusterPeerNotMemberErr ErrorIdentifier = 10040
+
+	// JSClusterRequiredErr JetStream clustering support required
+	JSClusterRequiredErr ErrorIdentifier = 10010
+
+	// JSClusterServerNotMemberErr server is not a member of the cluster
+	JSClusterServerNotMemberErr ErrorIdentifier = 10044
+
+	// JSClusterTagsErr tags placement not supported for operation
+	JSClusterTagsErr ErrorIdentifier = 10011
+
+	// JSClusterUnSupportFeatureErr not currently supported in clustered mode
+	JSClusterUnSupportFeatureErr ErrorIdentifier = 10036
+
+	// JSConsumerCreateErrF General consumer creation failure string ({err})
+	JSConsumerCreateErrF ErrorIdentifier = 10012
+
+	// JSConsumerDurableNameNotInSubjectErr consumer expected to be durable but no durable name set in subject
+	JSConsumerDurableNameNotInSubjectErr ErrorIdentifier = 10016
+
+	// JSConsumerDurableNameNotMatchSubjectErr consumer name in subject does not match durable name in request
+	JSConsumerDurableNameNotMatchSubjectErr ErrorIdentifier = 10017
+
+	// JSConsumerDurableNameNotSetErr consumer expected to be durable but a durable name was not set
+	JSConsumerDurableNameNotSetErr ErrorIdentifier = 10018
+
+	// JSConsumerEphemeralWithDurableInSubjectErr consumer expected to be ephemeral but detected a durable name set in subject
+	JSConsumerEphemeralWithDurableInSubjectErr ErrorIdentifier = 10019
+
+	// JSConsumerEphemeralWithDurableNameErr consumer expected to be ephemeral but a durable name was set in request
+	JSConsumerEphemeralWithDurableNameErr ErrorIdentifier = 10020
+
+	// JSConsumerNameExistErr consumer name already in use
+	JSConsumerNameExistErr ErrorIdentifier = 10013
+
+	// JSConsumerNotFoundErr consumer not found
+	JSConsumerNotFoundErr ErrorIdentifier = 10014
+
+	// JSInsufficientResourcesErr insufficient resources
+	JSInsufficientResourcesErr ErrorIdentifier = 10023
+
+	// JSInvalidJSONErr invalid JSON
+	JSInvalidJSONErr ErrorIdentifier = 10025
+
+	// JSMaximumConsumersLimitErr maximum consumers exceeds account limit
+	JSMaximumConsumersLimitErr ErrorIdentifier = 10026
+
+	// JSMaximumStreamsLimitErr maximum number of streams reached
+	JSMaximumStreamsLimitErr ErrorIdentifier = 10027
+
+	// JSMemoryResourcesExceededErr insufficient memory resources available
+	JSMemoryResourcesExceededErr ErrorIdentifier = 10028
+
+	// JSMirrorConsumerSetupFailedErrF Generic mirror consumer setup failure string ({err})
+	JSMirrorConsumerSetupFailedErrF ErrorIdentifier = 10029
+
+	// JSMirrorMaxMessageSizeTooBigErr stream mirror must have max message size >= source
+	JSMirrorMaxMessageSizeTooBigErr ErrorIdentifier = 10030
+
+	// JSMirrorWithSourcesErr stream mirrors can not also contain other sources
+	JSMirrorWithSourcesErr ErrorIdentifier = 10031
+
+	// JSMirrorWithStartSeqAndTimeErr stream mirrors can not have both start seq and start time configured
+	JSMirrorWithStartSeqAndTimeErr ErrorIdentifier = 10032
+
+	// JSMirrorWithSubjectFiltersErr stream mirrors can not contain filtered subjects
+	JSMirrorWithSubjectFiltersErr ErrorIdentifier = 10033
+
+	// JSMirrorWithSubjectsErr stream mirrors can not also contain subjects
+	JSMirrorWithSubjectsErr ErrorIdentifier = 10034
+
+	// JSNoAccountErr account not found
+	JSNoAccountErr ErrorIdentifier = 10035
+
+	// JSNoMessageFoundErr no message found
+	JSNoMessageFoundErr ErrorIdentifier = 10037
+
+	// JSNotEmptyRequestErr expected an empty request payload
+	JSNotEmptyRequestErr ErrorIdentifier = 10038
+
+	// JSNotEnabledErr JetStream not enabled
+	JSNotEnabledErr ErrorIdentifier = 10076
+
+	// JSNotEnabledForAccountErr JetStream not enabled for account
+	JSNotEnabledForAccountErr ErrorIdentifier = 10039
+
+	// JSPeerRemapErr peer remap failed
+	JSPeerRemapErr ErrorIdentifier = 10075
+
+	// JSRaftGeneralErrF General RAFT error string ({err})
+	JSRaftGeneralErrF ErrorIdentifier = 10041
+
+	// JSRestoreSubscribeFailedErrF JetStream unable to subscribe to restore snapshot {subject}: {err}
+	JSRestoreSubscribeFailedErrF ErrorIdentifier = 10042
+
+	// JSSequenceNotFoundErrF sequence {seq} not found
+	JSSequenceNotFoundErrF ErrorIdentifier = 10043
+
+	// JSSnapshotDeliverSubjectInvalidErr deliver subject not valid
+	JSSnapshotDeliverSubjectInvalidErr ErrorIdentifier = 10015
+
+	// JSSourceConsumerSetupFailedErrF General source consumer setup failure string ({err})
+	JSSourceConsumerSetupFailedErrF ErrorIdentifier = 10045
+
+	// JSSourceMaxMessageSizeTooBigErr stream source must have max message size >= target
+	JSSourceMaxMessageSizeTooBigErr ErrorIdentifier = 10046
+
+	// JSStorageResourcesExceededErr insufficient storage resources available
+	JSStorageResourcesExceededErr ErrorIdentifier = 10047
+
+	// JSStreamAssignmentErrF {err}
+	JSStreamAssignmentErrF ErrorIdentifier = 10048
+
+	// JSStreamCreateErrF Generic stream creation error string ({err})
+	JSStreamCreateErrF ErrorIdentifier = 10049
+
+	// JSStreamDeleteErrF General stream deletion error string ({err})
+	JSStreamDeleteErrF ErrorIdentifier = 10050
+
+	// JSStreamExternalApiOverlapErrF stream external api prefix {prefix} must not overlap with {subject}
+	JSStreamExternalApiOverlapErrF ErrorIdentifier = 10021
+
+	// JSStreamExternalDelPrefixOverlapsErrF stream external delivery prefix {prefix} overlaps with stream subject {subject}
+	JSStreamExternalDelPrefixOverlapsErrF ErrorIdentifier = 10022
+
+	// JSStreamGeneralErrorF General stream failure string
+	JSStreamGeneralErrorF ErrorIdentifier = 10051
+
+	// JSStreamInvalidConfigF Stream configuration validation error string ({err})
+	JSStreamInvalidConfigF ErrorIdentifier = 10052
+
+	// JSStreamInvalidExternalDeliverySubjErrF stream external delivery prefix {prefix} must not contain wildcards
+	JSStreamInvalidExternalDeliverySubjErrF ErrorIdentifier = 10024
+
+	// JSStreamLimitsErrF General stream limits exceeded error string ({err})
+	JSStreamLimitsErrF ErrorIdentifier = 10053
+
+	// JSStreamMessageExceedsMaximumErr message size exceeds maximum allowed
+	JSStreamMessageExceedsMaximumErr ErrorIdentifier = 10054
+
+	// JSStreamMirrorNotUpdatableErr Mirror configuration can not be updated
+	JSStreamMirrorNotUpdatableErr ErrorIdentifier = 10055
+
+	// JSStreamMismatchErr stream name in subject does not match request
+	JSStreamMismatchErr ErrorIdentifier = 10056
+
+	// JSStreamMsgDeleteFailedF Generic message deletion failure error string (%s)
+	JSStreamMsgDeleteFailedF ErrorIdentifier = 10057
+
+	// JSStreamNameExistErr stream name already in use
+	JSStreamNameExistErr ErrorIdentifier = 10058
+
+	// JSStreamNotFoundErr stream not found
+	JSStreamNotFoundErr ErrorIdentifier = 10059
+
+	// JSStreamNotMatchErr expected stream does not match
+	JSStreamNotMatchErr ErrorIdentifier = 10060
+
+	// JSStreamReplicasNotSupportedErr replicas > 1 not supported in non-clustered mode
+	JSStreamReplicasNotSupportedErr ErrorIdentifier = 10074
+
+	// JSStreamReplicasNotUpdatableErr Replicas configuration can not be updated
+	JSStreamReplicasNotUpdatableErr ErrorIdentifier = 10061
+
+	// JSStreamRestoreErrF restore failed: {err}
+	JSStreamRestoreErrF ErrorIdentifier = 10062
+
+	// JSStreamSequenceNotMatchErr expected stream sequence does not match
+	JSStreamSequenceNotMatchErr ErrorIdentifier = 10063
+
+	// JSStreamSnapshotErrF snapshot failed: {err}
+	JSStreamSnapshotErrF ErrorIdentifier = 10064
+
+	// JSStreamSubjectOverlapErr subjects overlap with an existing stream
+	JSStreamSubjectOverlapErr ErrorIdentifier = 10065
+
+	// JSStreamTemplateCreateErrF Generic template creation failed string ({err})
+	JSStreamTemplateCreateErrF ErrorIdentifier = 10066
+
+	// JSStreamTemplateDeleteErrF Generic stream template deletion failed error string ({err})
+	JSStreamTemplateDeleteErrF ErrorIdentifier = 10067
+
+	// JSStreamTemplateNotFoundErr template not found
+	JSStreamTemplateNotFoundErr ErrorIdentifier = 10068
+
+	// JSStreamUpdateErrF Generic stream update error string ({err})
+	JSStreamUpdateErrF ErrorIdentifier = 10069
+
+	// JSStreamWrongLastMsgIDErrF wrong last msg ID: {id}
+	JSStreamWrongLastMsgIDErrF ErrorIdentifier = 10070
+
+	// JSStreamWrongLastSequenceErrF wrong last sequence: {seq}
+	JSStreamWrongLastSequenceErrF ErrorIdentifier = 10071
+
+	// JSTempStorageFailedErr JetStream unable to open temp storage for restore
+	JSTempStorageFailedErr ErrorIdentifier = 10072
+
+	// JSTemplateNameNotMatchSubjectErr template name in subject does not match request
+	JSTemplateNameNotMatchSubjectErr ErrorIdentifier = 10073
+)
+
+var (
+	ApiErrors = map[ErrorIdentifier]*ApiError{
+		JSAccountResourcesExceededErr:              {Code: 400, ErrCode: 10002, Description: "resource limits exceeded for account"},
+		JSBadRequestErr:                            {Code: 400, ErrCode: 10003, Description: "bad request"},
+		JSClusterIncompleteErr:                     {Code: 503, ErrCode: 10004, Description: "incomplete results"},
+		JSClusterNoPeersErr:                        {Code: 400, ErrCode: 10005, Description: "no suitable peers for placement"},
+		JSClusterNotActiveErr:                      {Code: 500, ErrCode: 10006, Description: "JetStream not in clustered mode"},
+		JSClusterNotAssignedErr:                    {Code: 500, ErrCode: 10007, Description: "JetStream cluster not assigned to this server"},
+		JSClusterNotAvailErr:                       {Code: 503, ErrCode: 10008, Description: "JetStream system temporarily unavailable"},
+		JSClusterNotLeaderErr:                      {Code: 500, ErrCode: 10009, Description: "JetStream cluster can not handle request"},
+		JSClusterPeerNotMemberErr:                  {Code: 400, ErrCode: 10040, Description: "peer not a member"},
+		JSClusterRequiredErr:                       {Code: 503, ErrCode: 10010, Description: "JetStream clustering support required"},
+		JSClusterServerNotMemberErr:                {Code: 400, ErrCode: 10044, Description: "server is not a member of the cluster"},
+		JSClusterTagsErr:                           {Code: 400, ErrCode: 10011, Description: "tags placement not supported for operation"},
+		JSClusterUnSupportFeatureErr:               {Code: 503, ErrCode: 10036, Description: "not currently supported in clustered mode"},
+		JSConsumerCreateErrF:                       {Code: 500, ErrCode: 10012, Description: "{err}"},
+		JSConsumerDurableNameNotInSubjectErr:       {Code: 400, ErrCode: 10016, Description: "consumer expected to be durable but no durable name set in subject"},
+		JSConsumerDurableNameNotMatchSubjectErr:    {Code: 400, ErrCode: 10017, Description: "consumer name in subject does not match durable name in request"},
+		JSConsumerDurableNameNotSetErr:             {Code: 400, ErrCode: 10018, Description: "consumer expected to be durable but a durable name was not set"},
+		JSConsumerEphemeralWithDurableInSubjectErr: {Code: 400, ErrCode: 10019, Description: "consumer expected to be ephemeral but detected a durable name set in subject"},
+		JSConsumerEphemeralWithDurableNameErr:      {Code: 400, ErrCode: 10020, Description: "consumer expected to be ephemeral but a durable name was set in request"},
+		JSConsumerNameExistErr:                     {Code: 400, ErrCode: 10013, Description: "consumer name already in use"},
+		JSConsumerNotFoundErr:                      {Code: 404, ErrCode: 10014, Description: "consumer not found"},
+		JSInsufficientResourcesErr:                 {Code: 503, ErrCode: 10023, Description: "insufficient resources"},
+		JSInvalidJSONErr:                           {Code: 400, ErrCode: 10025, Description: "invalid JSON"},
+		JSMaximumConsumersLimitErr:                 {Code: 400, ErrCode: 10026, Description: "maximum consumers exceeds account limit"},
+		JSMaximumStreamsLimitErr:                   {Code: 400, ErrCode: 10027, Description: "maximum number of streams reached"},
+		JSMemoryResourcesExceededErr:               {Code: 500, ErrCode: 10028, Description: "insufficient memory resources available"},
+		JSMirrorConsumerSetupFailedErrF:            {Code: 500, ErrCode: 10029, Description: "{err}"},
+		JSMirrorMaxMessageSizeTooBigErr:            {Code: 400, ErrCode: 10030, Description: "stream mirror must have max message size >= source"},
+		JSMirrorWithSourcesErr:                     {Code: 400, ErrCode: 10031, Description: "stream mirrors can not also contain other sources"},
+		JSMirrorWithStartSeqAndTimeErr:             {Code: 400, ErrCode: 10032, Description: "stream mirrors can not have both start seq and start time configured"},
+		JSMirrorWithSubjectFiltersErr:              {Code: 400, ErrCode: 10033, Description: "stream mirrors can not contain filtered subjects"},
+		JSMirrorWithSubjectsErr:                    {Code: 400, ErrCode: 10034, Description: "stream mirrors can not also contain subjects"},
+		JSNoAccountErr:                             {Code: 503, ErrCode: 10035, Description: "account not found"},
+		JSNoMessageFoundErr:                        {Code: 404, ErrCode: 10037, Description: "no message found"},
+		JSNotEmptyRequestErr:                       {Code: 400, ErrCode: 10038, Description: "expected an empty request payload"},
+		JSNotEnabledErr:                            {Code: 503, ErrCode: 10076, Description: "JetStream not enabled", Help: "This error indicates that JetStream is not enabled at a global level"},
+		JSNotEnabledForAccountErr:                  {Code: 503, ErrCode: 10039, Description: "JetStream not enabled for account", Help: "This error indicates that JetStream is not enabled for an account account level"},
+		JSPeerRemapErr:                             {Code: 503, ErrCode: 10075, Description: "peer remap failed"},
+		JSRaftGeneralErrF:                          {Code: 500, ErrCode: 10041, Description: "{err}"},
+		JSRestoreSubscribeFailedErrF:               {Code: 500, ErrCode: 10042, Description: "JetStream unable to subscribe to restore snapshot {subject}: {err}"},
+		JSSequenceNotFoundErrF:                     {Code: 400, ErrCode: 10043, Description: "sequence {seq} not found"},
+		JSSnapshotDeliverSubjectInvalidErr:         {Code: 400, ErrCode: 10015, Description: "deliver subject not valid"},
+		JSSourceConsumerSetupFailedErrF:            {Code: 500, ErrCode: 10045, Description: "{err}"},
+		JSSourceMaxMessageSizeTooBigErr:            {Code: 400, ErrCode: 10046, Description: "stream source must have max message size >= target"},
+		JSStorageResourcesExceededErr:              {Code: 500, ErrCode: 10047, Description: "insufficient storage resources available"},
+		JSStreamAssignmentErrF:                     {Code: 500, ErrCode: 10048, Description: "{err}"},
+		JSStreamCreateErrF:                         {Code: 500, ErrCode: 10049, Description: "{err}"},
+		JSStreamDeleteErrF:                         {Code: 500, ErrCode: 10050, Description: "{err}"},
+		JSStreamExternalApiOverlapErrF:             {Code: 400, ErrCode: 10021, Description: "stream external api prefix {prefix} must not overlap with {subject}"},
+		JSStreamExternalDelPrefixOverlapsErrF:      {Code: 400, ErrCode: 10022, Description: "stream external delivery prefix {prefix} overlaps with stream subject {subject}"},
+		JSStreamGeneralErrorF:                      {Code: 500, ErrCode: 10051, Description: "General stream failure string"},
+		JSStreamInvalidConfigF:                     {Code: 500, ErrCode: 10052, Description: "{err}"},
+		JSStreamInvalidExternalDeliverySubjErrF:    {Code: 400, ErrCode: 10024, Description: "stream external delivery prefix {prefix} must not contain wildcards"},
+		JSStreamLimitsErrF:                         {Code: 500, ErrCode: 10053, Description: "{err}"},
+		JSStreamMessageExceedsMaximumErr:           {Code: 400, ErrCode: 10054, Description: "message size exceeds maximum allowed"},
+		JSStreamMirrorNotUpdatableErr:              {Code: 400, ErrCode: 10055, Description: "Mirror configuration can not be updated"},
+		JSStreamMismatchErr:                        {Code: 400, ErrCode: 10056, Description: "stream name in subject does not match request"},
+		JSStreamMsgDeleteFailedF:                   {Code: 500, ErrCode: 10057, Description: "%s"},
+		JSStreamNameExistErr:                       {Code: 400, ErrCode: 10058, Description: "stream name already in use"},
+		JSStreamNotFoundErr:                        {Code: 404, ErrCode: 10059, Description: "stream not found"},
+		JSStreamNotMatchErr:                        {Code: 400, ErrCode: 10060, Description: "expected stream does not match"},
+		JSStreamReplicasNotSupportedErr:            {Code: 500, ErrCode: 10074, Description: "replicas > 1 not supported in non-clustered mode"},
+		JSStreamReplicasNotUpdatableErr:            {Code: 400, ErrCode: 10061, Description: "Replicas configuration can not be updated"},
+		JSStreamRestoreErrF:                        {Code: 500, ErrCode: 10062, Description: "restore failed: {err}"},
+		JSStreamSequenceNotMatchErr:                {Code: 503, ErrCode: 10063, Description: "expected stream sequence does not match"},
+		JSStreamSnapshotErrF:                       {Code: 500, ErrCode: 10064, Description: "snapshot failed: {err}"},
+		JSStreamSubjectOverlapErr:                  {Code: 500, ErrCode: 10065, Description: "subjects overlap with an existing stream"},
+		JSStreamTemplateCreateErrF:                 {Code: 500, ErrCode: 10066, Description: "{err}"},
+		JSStreamTemplateDeleteErrF:                 {Code: 500, ErrCode: 10067, Description: "{err}"},
+		JSStreamTemplateNotFoundErr:                {Code: 404, ErrCode: 10068, Description: "template not found"},
+		JSStreamUpdateErrF:                         {Code: 500, ErrCode: 10069, Description: "{err}"},
+		JSStreamWrongLastMsgIDErrF:                 {Code: 400, ErrCode: 10070, Description: "wrong last msg ID: {id}"},
+		JSStreamWrongLastSequenceErrF:              {Code: 400, ErrCode: 10071, Description: "wrong last sequence: {seq}"},
+		JSTempStorageFailedErr:                     {Code: 500, ErrCode: 10072, Description: "JetStream unable to open temp storage for restore"},
+		JSTemplateNameNotMatchSubjectErr:           {Code: 400, ErrCode: 10073, Description: "template name in subject does not match request"},
+	}
+	// ErrJetStreamNotClustered Deprecated by JSClusterNotActiveErr ApiError, use IsNatsError() for comparisons
+	ErrJetStreamNotClustered = ApiErrors[JSClusterNotActiveErr]
+	// ErrJetStreamNotAssigned Deprecated by JSClusterNotAssignedErr ApiError, use IsNatsError() for comparisons
+	ErrJetStreamNotAssigned = ApiErrors[JSClusterNotAssignedErr]
+	// ErrJetStreamNotLeader Deprecated by JSClusterNotLeaderErr ApiError, use IsNatsError() for comparisons
+	ErrJetStreamNotLeader = ApiErrors[JSClusterNotLeaderErr]
+	// ErrJetStreamConsumerAlreadyUsed Deprecated by JSConsumerNameExistErr ApiError, use IsNatsError() for comparisons
+	ErrJetStreamConsumerAlreadyUsed = ApiErrors[JSConsumerNameExistErr]
+	// ErrJetStreamResourcesExceeded Deprecated by JSInsufficientResourcesErr ApiError, use IsNatsError() for comparisons
+	ErrJetStreamResourcesExceeded = ApiErrors[JSInsufficientResourcesErr]
+	// ErrMemoryResourcesExceeded Deprecated by JSMemoryResourcesExceededErr ApiError, use IsNatsError() for comparisons
+	ErrMemoryResourcesExceeded = ApiErrors[JSMemoryResourcesExceededErr]
+	// ErrJetStreamNotEnabled Deprecated by JSNotEnabledErr ApiError, use IsNatsError() for comparisons
+	ErrJetStreamNotEnabled = ApiErrors[JSNotEnabledErr]
+	// ErrStorageResourcesExceeded Deprecated by JSStorageResourcesExceededErr ApiError, use IsNatsError() for comparisons
+	ErrStorageResourcesExceeded = ApiErrors[JSStorageResourcesExceededErr]
+	// ErrJetStreamStreamAlreadyUsed Deprecated by JSStreamNameExistErr ApiError, use IsNatsError() for comparisons
+	ErrJetStreamStreamAlreadyUsed = ApiErrors[JSStreamNameExistErr]
+	// ErrJetStreamStreamNotFound Deprecated by JSStreamNotFoundErr ApiError, use IsNatsError() for comparisons
+	ErrJetStreamStreamNotFound = ApiErrors[JSStreamNotFoundErr]
+	// ErrReplicasNotSupported Deprecated by JSStreamReplicasNotSupportedErr ApiError, use IsNatsError() for comparisons
+	ErrReplicasNotSupported = ApiErrors[JSStreamReplicasNotSupportedErr]
+)
diff --git a/server/mqtt.go b/server/mqtt.go
index bc109c7f..279b0d13 100644
--- a/server/mqtt.go
+++ b/server/mqtt.go
@@ -962,7 +962,7 @@ func (s *Server) mqttCreateAccountSessionManager(acc *Account, quitCh chan struc
 		Retention: InterestPolicy,
 		Replicas:  as.replicas,
 	}
-	if _, err := jsa.createStream(cfg); isErrorOtherThan(err, ErrJetStreamStreamAlreadyUsed) {
+	if _, err := jsa.createStream(cfg); isErrorOtherThan(err, JSStreamNameExistErr) {
 		return nil, fmt.Errorf("create messages stream for account %q: %v", acc.GetName(), err)
 	}
 
@@ -975,7 +975,7 @@ func (s *Server) mqttCreateAccountSessionManager(acc *Account, quitCh chan struc
 		Replicas:  as.replicas,
 	}
 	si, err := jsa.createStream(cfg)
-	if isErrorOtherThan(err, ErrJetStreamStreamAlreadyUsed) {
+	if isErrorOtherThan(err, JSStreamNameExistErr) {
 		return nil, fmt.Errorf("create retained messages stream for account %q: %v", acc.GetName(), err)
 	}
 	if err != nil {
@@ -1110,18 +1110,6 @@ func (jsa *mqttJSA) newRequestEx(kind, subject string, hdr int, msg []byte, time
 	return i, nil
 }
 
-// If `e` is not nil, returns an error corresponding to e.Description, if not empty,
-// or an error of the form: "code %d".
-func convertApiErrorToError(e *ApiError) error {
-	if e == nil {
-		return nil
-	}
-	if e.Description == _EMPTY_ {
-		return fmt.Errorf("code %d", e.Code)
-	}
-	return errors.New(e.Description)
-}
-
 func (jsa *mqttJSA) createConsumer(cfg *CreateConsumerRequest) (*JSApiConsumerCreateResponse, error) {
 	cfgb, err := json.Marshal(cfg)
 	if err != nil {
@@ -1138,7 +1126,7 @@ func (jsa *mqttJSA) createConsumer(cfg *CreateConsumerRequest) (*JSApiConsumerCr
 		return nil, err
 	}
 	ccr := ccri.(*JSApiConsumerCreateResponse)
-	return ccr, convertApiErrorToError(ccr.Error)
+	return ccr, ccr.ToError()
 }
 
 func (jsa *mqttJSA) deleteConsumer(streamName, consName string) (*JSApiConsumerDeleteResponse, error) {
@@ -1148,7 +1136,7 @@ func (jsa *mqttJSA) deleteConsumer(streamName, consName string) (*JSApiConsumerD
 		return nil, err
 	}
 	cdr := cdri.(*JSApiConsumerDeleteResponse)
-	return cdr, convertApiErrorToError(cdr.Error)
+	return cdr, cdr.ToError()
 }
 
 func (jsa *mqttJSA) createStream(cfg *StreamConfig) (*StreamInfo, error) {
@@ -1161,7 +1149,7 @@ func (jsa *mqttJSA) createStream(cfg *StreamConfig) (*StreamInfo, error) {
 		return nil, err
 	}
 	scr := scri.(*JSApiStreamCreateResponse)
-	return scr.StreamInfo, convertApiErrorToError(scr.Error)
+	return scr.StreamInfo, scr.ToError()
 }
 
 func (jsa *mqttJSA) lookupStream(name string) (*StreamInfo, error) {
@@ -1170,7 +1158,7 @@ func (jsa *mqttJSA) lookupStream(name string) (*StreamInfo, error) {
 		return nil, err
 	}
 	slr := slri.(*JSApiStreamInfoResponse)
-	return slr.StreamInfo, convertApiErrorToError(slr.Error)
+	return slr.StreamInfo, slr.ToError()
 }
 
 func (jsa *mqttJSA) deleteStream(name string) (bool, error) {
@@ -1179,7 +1167,7 @@ func (jsa *mqttJSA) deleteStream(name string) (bool, error) {
 		return false, err
 	}
 	sdr := sdri.(*JSApiStreamDeleteResponse)
-	return sdr.Success, convertApiErrorToError(sdr.Error)
+	return sdr.Success, sdr.ToError()
 }
 
 func (jsa *mqttJSA) loadMsg(streamName string, seq uint64) (*StoredMsg, error) {
@@ -1193,7 +1181,7 @@ func (jsa *mqttJSA) loadMsg(streamName string, seq uint64) (*StoredMsg, error) {
 		return nil, err
 	}
 	lmr := lmri.(*JSApiMsgGetResponse)
-	return lmr.Message, convertApiErrorToError(lmr.Error)
+	return lmr.Message, lmr.ToError()
 }
 
 func (jsa *mqttJSA) storeMsg(subject string, headers int, msg []byte) (*JSPubAckResponse, error) {
@@ -1206,7 +1194,7 @@ func (jsa *mqttJSA) storeMsgWithKind(kind, subject string, headers int, msg []by
 		return nil, err
 	}
 	smr := smri.(*JSPubAckResponse)
-	return smr, convertApiErrorToError(smr.Error)
+	return smr, smr.ToError()
 }
 
 func (jsa *mqttJSA) deleteMsg(stream string, seq uint64) {
@@ -1224,11 +1212,9 @@ func (jsa *mqttJSA) deleteMsg(stream string, seq uint64) {
 //
 //////////////////////////////////////////////////////////////////////////////
 
-// Returns true if `err1` is not nil and does not match `err2`, that is
-// their error strings are different.
-// Assumes that `err2` is never nil.
-func isErrorOtherThan(err1, err2 error) bool {
-	return err1 != nil && err1.Error() != err2.Error()
+// Returns true if `err` is not nil and does not match the api error with ErrorIdentifier id
+func isErrorOtherThan(err error, id ErrorIdentifier) bool {
+	return err != nil && !IsNatsErr(err, id)
 }
 
 // Process JS API replies.
@@ -1250,43 +1236,43 @@ func (as *mqttAccountSessionManager) processJSAPIReplies(_ *subscription, pc *cl
 	case mqttJSAStreamCreate:
 		var resp = &JSApiStreamCreateResponse{}
 		if err := json.Unmarshal(msg, resp); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 		}
 		ch <- resp
 	case mqttJSAStreamLookup:
 		var resp = &JSApiStreamInfoResponse{}
 		if err := json.Unmarshal(msg, &resp); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 		}
 		ch <- resp
 	case mqttJSAStreamDel:
 		var resp = &JSApiStreamDeleteResponse{}
 		if err := json.Unmarshal(msg, &resp); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 		}
 		ch <- resp
 	case mqttJSAConsumerCreate:
 		var resp = &JSApiConsumerCreateResponse{}
 		if err := json.Unmarshal(msg, resp); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 		}
 		ch <- resp
 	case mqttJSAConsumerDel:
 		var resp = &JSApiConsumerDeleteResponse{}
 		if err := json.Unmarshal(msg, resp); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 		}
 		ch <- resp
 	case mqttJSAMsgStore, mqttJSASessPersist:
 		var resp = &JSPubAckResponse{}
 		if err := json.Unmarshal(msg, resp); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 		}
 		ch <- resp
 	case mqttJSAMsgLoad:
 		var resp = &JSApiMsgGetResponse{}
 		if err := json.Unmarshal(msg, resp); err != nil {
-			resp.Error = jsInvalidJSONErr
+			resp.Error = ApiErrors[JSInvalidJSONErr]
 		}
 		ch <- resp
 	default:
@@ -1361,7 +1347,7 @@ func (as *mqttAccountSessionManager) processSessionPersist(_ *subscription, pc *
 	if err := json.Unmarshal(msg, par); err != nil {
 		return
 	}
-	if err := convertApiErrorToError(par.Error); err != nil {
+	if err := par.Error; err != nil {
 		return
 	}
 	cIDHash := strings.TrimPrefix(par.Stream, mqttSessionsStreamNamePrefix)
@@ -1889,13 +1875,13 @@ CREATE_STREAM:
 	if err != nil {
 		// Check for insufficient resources. If that is the case, and if possible, try
 		// again with a lower replicas value.
-		if cfg.Replicas > 1 && err.Error() == jsInsufficientErr.Description {
+		if cfg.Replicas > 1 && IsNatsErr(err, JSInsufficientResourcesErr) {
 			cfg.Replicas--
 			goto CREATE_STREAM
 		}
 		// If there is an error and not simply "already used" (which means that the
 		// stream already exists) then we fail.
-		if isErrorOtherThan(err, ErrJetStreamStreamAlreadyUsed) {
+		if isErrorOtherThan(err, JSStreamNameExistErr) {
 			return formatError("create session stream", err)
 		}
 	}
diff --git a/server/stream.go b/server/stream.go
index 208c6463..c257ea49 100644
--- a/server/stream.go
+++ b/server/stream.go
@@ -57,14 +57,21 @@ type StreamConfig struct {
 	Sources      []*StreamSource `json:"sources,omitempty"`
 }
 
-const JSApiPubAckResponseType = "io.nats.jetstream.api.v1.pub_ack_response"
-
 // JSPubAckResponse is a formal response to a publish operation.
 type JSPubAckResponse struct {
 	Error *ApiError `json:"error,omitempty"`
 	*PubAck
 }
 
+// ToError checks if the response has a error and if it does converts it to an error avoiding the pitfalls described by https://yourbasic.org/golang/gotcha-why-nil-error-not-equal-nil/
+func (r *JSPubAckResponse) ToError() error {
+	if r.Error == nil {
+		return nil
+	}
+
+	return r.Error
+}
+
 // PubAck is the detail you get back from a publish to a stream that was successful.
 // e.g. +OK {"stream": "Orders", "seq": 22}
 type PubAck struct {
@@ -220,8 +227,7 @@ type ddentry struct {
 
 // Replicas Range
 const (
-	StreamDefaultReplicas = 1
-	StreamMaxReplicas     = 5
+	StreamMaxReplicas = 5
 )
 
 // AddStream adds a stream for the given account.
@@ -250,12 +256,12 @@ func (a *Account) addStreamWithAssignment(config *StreamConfig, fsConfig *FileSt
 	// Sensible defaults.
 	cfg, err := checkStreamCfg(config)
 	if err != nil {
-		return nil, err
+		return nil, ApiErrors[JSStreamInvalidConfigF].ErrOrNewT(err, "{err}", err)
 	}
 
 	singleServerMode := !s.JetStreamIsClustered() && s.standAloneMode()
 	if singleServerMode && cfg.Replicas > 1 {
-		return nil, ErrReplicasNotSupported
+		return nil, ApiErrors[JSStreamReplicasNotSupportedErr]
 	}
 
 	jsa.mu.Lock()
@@ -270,7 +276,7 @@ func (a *Account) addStreamWithAssignment(config *StreamConfig, fsConfig *FileSt
 			}
 			return mset, nil
 		} else {
-			return nil, ErrJetStreamStreamAlreadyUsed
+			return nil, ApiErrors[JSStreamNameExistErr]
 		}
 	}
 	// Check for limits.
@@ -731,7 +737,7 @@ func (jsa *jsAccount) subjectsOverlap(subjects []string) bool {
 	return false
 }
 
-// Default duplicates window.
+// StreamDefaultDuplicatesWindow default duplicates window.
 const StreamDefaultDuplicatesWindow = 2 * time.Minute
 
 func checkStreamCfg(config *StreamConfig) (StreamConfig, error) {
@@ -824,31 +830,31 @@ func (mset *stream) fileStoreConfig() (FileStoreConfig, error) {
 func (jsa *jsAccount) configUpdateCheck(old, new *StreamConfig) (*StreamConfig, error) {
 	cfg, err := checkStreamCfg(new)
 	if err != nil {
-		return nil, err
+		return nil, ApiErrors[JSStreamInvalidConfigF].ErrOrNewT(err, "{err}", err)
 	}
 
 	// Name must match.
 	if cfg.Name != old.Name {
-		return nil, fmt.Errorf("stream configuration name must match original")
+		return nil, ApiErrors[JSStreamInvalidConfigF].NewT("{err}", "stream configuration name must match original")
 	}
 	// Can't change MaxConsumers for now.
 	if cfg.MaxConsumers != old.MaxConsumers {
-		return nil, fmt.Errorf("stream configuration update can not change MaxConsumers")
+		return nil, ApiErrors[JSStreamInvalidConfigF].NewT("{err}", "stream configuration update can not change MaxConsumers")
 	}
 	// Can't change storage types.
 	if cfg.Storage != old.Storage {
-		return nil, fmt.Errorf("stream configuration update can not change storage type")
+		return nil, ApiErrors[JSStreamInvalidConfigF].NewT("{err}", "stream configuration update can not change storage type")
 	}
 	// Can't change retention.
 	if cfg.Retention != old.Retention {
-		return nil, fmt.Errorf("stream configuration update can not change retention policy")
+		return nil, ApiErrors[JSStreamInvalidConfigF].NewT("{err}", "stream configuration update can not change retention policy")
 	}
 	// Can not have a template owner for now.
 	if old.Template != _EMPTY_ {
-		return nil, fmt.Errorf("stream configuration update not allowed on template owned stream")
+		return nil, ApiErrors[JSStreamInvalidConfigF].NewT("{err}", "stream configuration update not allowed on template owned stream")
 	}
 	if cfg.Template != _EMPTY_ {
-		return nil, fmt.Errorf("stream configuration update can not be owned by a template")
+		return nil, ApiErrors[JSStreamInvalidConfigF].NewT("{err}", "stream configuration update can not be owned by a template")
 	}
 
 	// Check limits.
@@ -863,7 +869,7 @@ func (mset *stream) update(config *StreamConfig) error {
 	ocfg := mset.config()
 	cfg, err := mset.jsa.configUpdateCheck(&ocfg, config)
 	if err != nil {
-		return err
+		return ApiErrors[JSStreamInvalidConfigF].ErrOrNewT(err, "{err}", err)
 	}
 
 	mset.mu.Lock()
@@ -1323,7 +1329,7 @@ func (mset *stream) processInboundMirrorMsg(m *inMsg) bool {
 	if node != nil {
 		if js.limitsExceeded(stype) {
 			s.resourcesExeededError()
-			err = ErrJetStreamResourcesExceeded
+			err = ApiErrors[JSInsufficientResourcesErr]
 		} else {
 			err = node.Propose(encodeStreamMsg(m.subj, _EMPTY_, m.hdr, m.msg, sseq-1, ts))
 		}
@@ -1506,7 +1512,7 @@ func (mset *stream) setupMirrorConsumer() error {
 		if err := json.Unmarshal(msg, &ccr); err != nil {
 			c.Warnf("JetStream bad mirror consumer create response: %q", msg)
 			mset.cancelMirrorConsumer()
-			mset.setMirrorErr(jsInvalidJSONErr)
+			mset.setMirrorErr(ApiErrors[JSInvalidJSONErr])
 			return
 		}
 		respCh <- &ccr
@@ -1555,7 +1561,7 @@ func (mset *stream) setupMirrorConsumer() error {
 					mset.queueInbound(msgs, subject, reply, hdr, msg)
 				})
 				if err != nil {
-					mset.mirror.err = jsError(err)
+					mset.mirror.err = ApiErrors[JSMirrorConsumerSetupFailedErrF].ErrOrNewT(err, "{err}", err)
 					mset.mirror.sub = nil
 					mset.mirror.cname = _EMPTY_
 				} else {
@@ -1739,7 +1745,7 @@ func (mset *stream) setSourceConsumer(iname string, seq uint64) {
 						mset.queueInbound(si.msgs, subject, reply, hdr, msg)
 					})
 					if err != nil {
-						si.err = jsError(err)
+						si.err = ApiErrors[JSSourceConsumerSetupFailedErrF].ErrOrNewT(err, "{err}", err)
 						si.sub = nil
 					} else {
 						si.err = nil
@@ -2531,7 +2537,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte,
 			mset.mu.Unlock()
 			if canRespond && outq != nil {
 				resp.PubAck = &PubAck{Stream: name}
-				resp.Error = &ApiError{Code: 503, Description: "expected stream sequence does not match"}
+				resp.Error = ApiErrors[JSStreamSequenceNotMatchErr]
 				b, _ := json.Marshal(resp)
 				outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil})
 			}
@@ -2567,7 +2573,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte,
 			mset.mu.Unlock()
 			if canRespond {
 				resp.PubAck = &PubAck{Stream: name}
-				resp.Error = &ApiError{Code: 400, Description: "expected stream does not match"}
+				resp.Error = ApiErrors[JSStreamNotMatchErr]
 				b, _ := json.Marshal(resp)
 				outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil})
 			}
@@ -2580,7 +2586,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte,
 			mset.mu.Unlock()
 			if canRespond {
 				resp.PubAck = &PubAck{Stream: name}
-				resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("wrong last sequence: %d", mlseq)}
+				resp.Error = ApiErrors[JSStreamWrongLastSequenceErrF].NewT("{seq}", mlseq)
 				b, _ := json.Marshal(resp)
 				outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil})
 			}
@@ -2593,7 +2599,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte,
 			mset.mu.Unlock()
 			if canRespond {
 				resp.PubAck = &PubAck{Stream: name}
-				resp.Error = &ApiError{Code: 400, Description: fmt.Sprintf("wrong last msg ID: %s", last)}
+				resp.Error = ApiErrors[JSStreamWrongLastMsgIDErrF].NewT("{id}", last)
 				b, _ := json.Marshal(resp)
 				outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil})
 			}
@@ -2614,7 +2620,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte,
 		mset.mu.Unlock()
 		if canRespond {
 			resp.PubAck = &PubAck{Stream: name}
-			resp.Error = &ApiError{Code: 400, Description: "message size exceeds maximum allowed"}
+			resp.Error = ApiErrors[JSStreamMessageExceedsMaximumErr]
 			b, _ := json.Marshal(resp)
 			mset.outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil})
 		}
@@ -2628,7 +2634,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte,
 		mset.mu.Unlock()
 		if canRespond {
 			resp.PubAck = &PubAck{Stream: name}
-			resp.Error = jsInsufficientErr
+			resp.Error = ApiErrors[JSInsufficientResourcesErr]
 			b, _ := json.Marshal(resp)
 			mset.outq.send(&jsPubMsg{reply, _EMPTY_, _EMPTY_, nil, b, nil, 0, nil})
 		}
@@ -2636,7 +2642,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte,
 		if node := mset.raftNode(); node != nil {
 			node.StepDown()
 		}
-		return ErrJetStreamResourcesExceeded
+		return ApiErrors[JSInsufficientResourcesErr]
 	}
 
 	var noInterest bool
@@ -2722,7 +2728,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte,
 		s.Warnf("JetStream resource limits exceeded for account: %q", accName)
 		if canRespond {
 			resp.PubAck = &PubAck{Stream: name}
-			resp.Error = &ApiError{Code: 400, Description: "resource limits exceeded for account"}
+			resp.Error = ApiErrors[JSAccountResourcesExceededErr]
 			response, _ = json.Marshal(resp)
 		}
 		// If we did not succeed put those values back.
@@ -2943,7 +2949,7 @@ func (mset *stream) stop(deleteFlag, advisory bool) error {
 	mset.mu.RUnlock()
 
 	if jsa == nil {
-		return ErrJetStreamNotEnabledForAccount
+		return ApiErrors[JSNotEnabledForAccountErr]
 	}
 
 	// Remove from our account map.
@@ -3235,7 +3241,7 @@ func (a *Account) RestoreStream(ncfg *StreamConfig, r io.Reader) (*stream, error
 
 	cfg, err := checkStreamCfg(ncfg)
 	if err != nil {
-		return nil, err
+		return nil, ApiErrors[JSStreamNotFoundErr].ErrOrNewT(err, "{err}", err)
 	}
 
 	_, jsa, err := a.checkForJetStream()
@@ -3297,7 +3303,7 @@ func (a *Account) RestoreStream(ncfg *StreamConfig, r io.Reader) (*stream, error
 
 	// See if this stream already exists.
 	if _, err := a.lookupStream(cfg.Name); err == nil {
-		return nil, ErrJetStreamStreamAlreadyUsed
+		return nil, ApiErrors[JSStreamNameExistErr]
 	}
 	// Move into the correct place here.
 	ndir := path.Join(jsa.storeDir, streamsDir, cfg.Name)


Test patch:
diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml
index b2149622..1b1df712 100644
--- a/.github/workflows/go-test.yaml
+++ b/.github/workflows/go-test.yaml
@@ -43,8 +43,8 @@ jobs:
         shell: bash --noprofile --norc -x -eo pipefail {0}
         run: |
           set -e
-          go test -i ./...
-          go test -v -run=TestNoRace --failfast -p=1 ./...
+          go test -vet=off -i ./...
+          go test -vet=off -v -run=TestNoRace --failfast -p=1 ./...
           # coverage via cov.sh disabled while just testing the waters
-          go test -v -race -p=1 --failfast ./...
+          go test -vet=off -v -race -p=1 --failfast ./...
           set +e
diff --git a/server/jetstream_cluster_test.go b/server/jetstream_cluster_test.go
index b580f972..378c2c09 100644
--- a/server/jetstream_cluster_test.go
+++ b/server/jetstream_cluster_test.go
@@ -140,7 +140,7 @@ func TestJetStreamClusterStreamLimitWithAccountDefaults(t *testing.T) {
 		MaxBytes: 15 * 1024 * 1024,
 	})
 	if err == nil || !strings.Contains(err.Error(), "insufficient storage") {
-		t.Fatalf("Expected %v but got %v", ErrStorageResourcesExceeded, err)
+		t.Fatalf("Expected %v but got %v", ApiErrors[JSStorageResourcesExceededErr], err)
 	}
 }
 
@@ -2559,7 +2559,7 @@ func TestJetStreamClusterUserSnapshotAndRestore(t *testing.T) {
 		t.Fatalf("Unexpected error: %v", err)
 	}
 	json.Unmarshal(rmsg.Data, &rresp)
-	if rresp.Error == nil || rresp.Error.Code != 500 || !strings.Contains(rresp.Error.Description, "already in use") {
+	if !IsNatsErr(rresp.Error, JSStreamNameExistErr) {
 		t.Fatalf("Did not get correct error response: %+v", rresp.Error)
 	}
 
@@ -4914,7 +4914,7 @@ func TestJetStreamFailMirrorsAndSources(t *testing.T) {
 	nc2, _ := jsClientConnect(t, s, nats.UserInfo("rip", "pass"))
 	defer nc2.Close()
 
-	testPrefix := func(testName, error string, cfg StreamConfig) {
+	testPrefix := func(testName string, id ErrorIdentifier, cfg StreamConfig) {
 		t.Run(testName, func(t *testing.T) {
 			req, err := json.Marshal(cfg)
 			if err != nil {
@@ -4930,13 +4930,13 @@ func TestJetStreamFailMirrorsAndSources(t *testing.T) {
 			}
 			if scResp.Error == nil {
 				t.Fatalf("Did expect an error but got none")
-			} else if !strings.Contains(scResp.Error.Description, error) {
+			} else if !IsNatsErr(scResp.Error, id) {
 				t.Fatalf("Expected different error: %s", scResp.Error.Description)
 			}
 		})
 	}
 
-	testPrefix("mirror-bad-deliverprefix", `prefix "test" overlaps with stream subject "test.>"`, StreamConfig{
+	testPrefix("mirror-bad-deliverprefix", JSStreamExternalDelPrefixOverlapsErrF, StreamConfig{
 		Name:    "MY_MIRROR_TEST",
 		Storage: FileStorage,
 		Mirror: &StreamSource{
@@ -4948,7 +4948,7 @@ func TestJetStreamFailMirrorsAndSources(t *testing.T) {
 			},
 		},
 	})
-	testPrefix("mirror-bad-apiprefix", `api prefix "$JS.API" must not overlap`, StreamConfig{
+	testPrefix("mirror-bad-apiprefix", JSStreamExternalApiOverlapErrF, StreamConfig{
 		Name:    "MY_MIRROR_TEST",
 		Storage: FileStorage,
 		Mirror: &StreamSource{
@@ -4959,7 +4959,7 @@ func TestJetStreamFailMirrorsAndSources(t *testing.T) {
 			},
 		},
 	})
-	testPrefix("source-bad-deliverprefix", `prefix "test" overlaps with stream subject "test.>"`, StreamConfig{
+	testPrefix("source-bad-deliverprefix", JSStreamExternalDelPrefixOverlapsErrF, StreamConfig{
 		Name:    "MY_SOURCE_TEST",
 		Storage: FileStorage,
 		Sources: []*StreamSource{{
@@ -4971,7 +4971,7 @@ func TestJetStreamFailMirrorsAndSources(t *testing.T) {
 		},
 		},
 	})
-	testPrefix("source-bad-apiprefix", `api prefix "$JS.API" must not overlap`, StreamConfig{
+	testPrefix("source-bad-apiprefix", JSStreamExternalApiOverlapErrF, StreamConfig{
 		Name:    "MY_SOURCE_TEST",
 		Storage: FileStorage,
 		Sources: []*StreamSource{{
@@ -6112,7 +6112,7 @@ func TestJetStreamClusterDomains(t *testing.T) {
 	// The domain signals to the system that we are our own JetStream domain and should not extend CORE.
 	// We want to check to make sure we have all the deny properly setup.
 	spoke.mu.Lock()
-	//var hasDE, hasDI bool
+	// var hasDE, hasDI bool
 	for _, ln := range spoke.leafs {
 		ln.mu.Lock()
 		remote := ln.leaf.remote
diff --git a/server/jetstream_errors_test.go b/server/jetstream_errors_test.go
new file mode 100644
index 00000000..8171de92
--- /dev/null
+++ b/server/jetstream_errors_test.go
@@ -0,0 +1,98 @@
+package server
+
+import (
+	"errors"
+	"fmt"
+	"testing"
+	"time"
+)
+
+func TestIsNatsErr(t *testing.T) {
+	if !IsNatsErr(ApiErrors[JSNotEnabledForAccountErr], JSNotEnabledForAccountErr) {
+		t.Fatalf("Expected error match")
+	}
+
+	if IsNatsErr(ApiErrors[JSNotEnabledForAccountErr], JSClusterNotActiveErr) {
+		t.Fatalf("Expected error mismatch")
+	}
+
+	if IsNatsErr(ApiErrors[JSNotEnabledForAccountErr], JSClusterNotActiveErr, JSClusterNotAvailErr) {
+		t.Fatalf("Expected error mismatch")
+	}
+
+	if !IsNatsErr(ApiErrors[JSNotEnabledForAccountErr], JSClusterNotActiveErr, JSNotEnabledForAccountErr) {
+		t.Fatalf("Expected error match")
+	}
+
+	if !IsNatsErr(&ApiError{ErrCode: 10039}, 1, JSClusterNotActiveErr, JSNotEnabledForAccountErr) {
+		t.Fatalf("Expected error match")
+	}
+
+	if IsNatsErr(&ApiError{ErrCode: 10039}, 1, 2, JSClusterNotActiveErr) {
+		t.Fatalf("Expected error mismatch")
+	}
+
+	if IsNatsErr(nil, JSClusterNotActiveErr) {
+		t.Fatalf("Expected error mismatch")
+	}
+
+	if IsNatsErr(errors.New("x"), JSClusterNotActiveErr) {
+		t.Fatalf("Expected error mismatch")
+	}
+}
+
+func TestApiError_Error(t *testing.T) {
+	if es := ApiErrors[JSClusterNotActiveErr].Error(); es != "JetStream not in clustered mode (10006)" {
+		t.Fatalf("Expected 'JetStream not in clustered mode (10006)', got %q", es)
+	}
+}
+
+func TestApiError_NewF(t *testing.T) {
+	ne := ApiErrors[JSRestoreSubscribeFailedErrF].NewT("{subject}", "the.subject", "{err}", errors.New("failed error"))
+	if ne.Description != "JetStream unable to subscribe to restore snapshot the.subject: failed error" {
+		t.Fatalf("Expected 'JetStream unable to subscribe to restore snapshot the.subject: failed error' got %q", ne.Description)
+	}
+
+	if ne == ApiErrors[JSRestoreSubscribeFailedErrF] {
+		t.Fatalf("Expected a new instance")
+	}
+}
+
+func TestApiError_ErrOrNewF(t *testing.T) {
+	if ne := ApiErrors[JSStreamRestoreErrF].ErrOrNewT(ApiErrors[JSNotEnabledForAccountErr], "{err}", errors.New("failed error")); !IsNatsErr(ne, JSNotEnabledForAccountErr) {
+		t.Fatalf("Expected JSNotEnabledForAccountErr got %s", ne)
+	}
+
+	if ne := ApiErrors[JSStreamRestoreErrF].ErrOrNewT(nil, "{err}", errors.New("failed error")); !IsNatsErr(ne, JSStreamRestoreErrF) {
+		t.Fatalf("Expected JSStreamRestoreErrF got %s", ne)
+	}
+
+	if ne := ApiErrors[JSStreamRestoreErrF].ErrOrNewT(errors.New("other error"), "{err}", errors.New("failed error")); !IsNatsErr(ne, JSStreamRestoreErrF) {
+		t.Fatalf("Expected JSStreamRestoreErrF got %s", ne)
+	}
+}
+
+func TestApiError_ErrOr(t *testing.T) {
+	if ne := ApiErrors[JSPeerRemapErr].ErrOr(ApiErrors[JSNotEnabledForAccountErr]); !IsNatsErr(ne, JSNotEnabledForAccountErr) {
+		t.Fatalf("Expected JSNotEnabledForAccountErr got %s", ne)
+	}
+
+	if ne := ApiErrors[JSPeerRemapErr].ErrOr(nil); !IsNatsErr(ne, JSPeerRemapErr) {
+		t.Fatalf("Expected JSPeerRemapErr got %s", ne)
+	}
+
+	if ne := ApiErrors[JSPeerRemapErr].ErrOr(errors.New("other error")); !IsNatsErr(ne, JSPeerRemapErr) {
+		t.Fatalf("Expected JSPeerRemapErr got %s", ne)
+	}
+}
+
+func TestApiError_NewT(t *testing.T) {
+	aerr := ApiError{
+		Code:        999,
+		Description: "thing {string} failed on attempt {int} after {duration} with {float}: {err}",
+	}
+
+	if ne := aerr.NewT("{float}", 1.1, "{err}", fmt.Errorf("simulated error"), "{string}", "hello world", "{int}", 10, "{duration}", 456*time.Millisecond); ne.Description != "thing hello world failed on attempt 10 after 456ms with 1.1: simulated error" {
+		t.Fatalf("Expected formatted error, got: %q", ne.Description)
+	}
+}
diff --git a/server/jetstream_test.go b/server/jetstream_test.go
index 57a5b46f..be8d1f25 100644
--- a/server/jetstream_test.go
+++ b/server/jetstream_test.go
@@ -827,7 +827,7 @@ func TestJetStreamAddStreamCanonicalNames(t *testing.T) {
 
 	expectErr := func(_ *stream, err error) {
 		t.Helper()
-		if err == nil || !strings.Contains(err.Error(), "can not contain") {
+		if !IsNatsErr(err, JSStreamInvalidConfigF) {
 			t.Fatalf("Expected error but got none")
 		}
 	}
@@ -4292,7 +4292,7 @@ func TestJetStreamSnapshotsAPI(t *testing.T) {
 		t.Fatalf("Unexpected error on snapshot request: %v", err)
 	}
 	json.Unmarshal(rmsg.Data, &rresp)
-	if rresp.Error == nil || rresp.Error.Code != 500 || !strings.Contains(rresp.Error.Description, "already in use") {
+	if !IsNatsErr(rresp.Error, JSStreamNameExistErr) {
 		t.Fatalf("Did not get correct error response: %+v", rresp.Error)
 	}
 
@@ -7417,34 +7417,13 @@ func TestJetStreamRequestAPI(t *testing.T) {
 		t.Fatalf("Created time seems wrong: %v\n", scResp.Created)
 	}
 
-	checkBadRequest := func(e *ApiError, description string) {
-		t.Helper()
-		if e == nil || e.Code != 400 || e.Description != description {
-			t.Fatalf("Did not get proper error: %+v", e)
-		}
-	}
-
-	checkServerError := func(e *ApiError, description string) {
-		t.Helper()
-		if e == nil || e.Code != 500 || e.Description != description {
-			t.Fatalf("Did not get proper server error: %+v\n", e)
-		}
-	}
-
-	checkNotFound := func(e *ApiError, description string) {
-		t.Helper()
-		if e == nil || e.Code != 404 || e.Description != description {
-			t.Fatalf("Did not get proper server error: %+v\n", e)
-		}
-	}
-
 	// Check that the name in config has to match the name in the subject
 	resp, _ = nc.Request(fmt.Sprintf(JSApiStreamCreateT, "BOB"), req, time.Second)
 	scResp.Error, scResp.StreamInfo = nil, nil
 	if err := json.Unmarshal(resp.Data, &scResp); err != nil {
 		t.Fatalf("Unexpected error: %v", err)
 	}
-	checkBadRequest(scResp.Error, "stream name in subject does not match request")
+	checkNatsError(t, scResp.Error, JSStreamMismatchErr)
 
 	// Check that update works.
 	msetCfg.Subjects = []string{"foo", "bar", "baz"}
@@ -7569,7 +7548,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
 	if err = json.Unmarshal(resp.Data, &bResp); err != nil {
 		t.Fatalf("Unexpected error: %v", err)
 	}
-	checkNotFound(bResp.Error, "stream not found")
+	checkNatsError(t, bResp.Error, JSStreamNotFoundErr)
 
 	// Now create a consumer.
 	delivery := nats.NewInbox()
@@ -7589,7 +7568,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
 	if err = json.Unmarshal(resp.Data, &ccResp); err != nil {
 		t.Fatalf("Unexpected error: %v", err)
 	}
-	checkServerError(ccResp.Error, "consumer requires interest for delivery subject when ephemeral")
+	checkNatsError(t, ccResp.Error, JSConsumerCreateErrF)
 
 	// Now create subscription and make sure we get proper response.
 	sub, _ := nc.SubscribeSync(delivery)
@@ -7627,7 +7606,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
 		t.Fatalf("Unexpected error: %v", err)
 	}
 	// Since we do not have interest this should have failed.
-	checkBadRequest(ccResp.Error, "stream name in subject does not match request")
+	checkNatsError(t, ccResp.Error, JSStreamMismatchErr)
 
 	// Get the list of all of the consumers for our stream.
 	resp, err = nc.Request(fmt.Sprintf(JSApiConsumersT, msetCfg.Name), nil, time.Second)
@@ -7697,7 +7676,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
 	if err = json.Unmarshal(resp.Data, &ccResp); err != nil {
 		t.Fatalf("Unexpected error: %v", err)
 	}
-	checkBadRequest(ccResp.Error, "consumer expected to be ephemeral but a durable name was set in request")
+	checkNatsError(t, ccResp.Error, JSConsumerEphemeralWithDurableNameErr)
 
 	// Now make sure we can create a durable on the subject with the proper name.
 	resp, err = nc.Request(fmt.Sprintf(JSApiDurableCreateT, msetCfg.Name, obsReq.Config.Durable), req, time.Second)
@@ -7726,7 +7705,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
 	if err = json.Unmarshal(resp.Data, &ccResp); err != nil {
 		t.Fatalf("Unexpected error: %v", err)
 	}
-	checkBadRequest(ccResp.Error, "consumer expected to be durable but a durable name was not set")
+	checkNatsError(t, ccResp.Error, JSConsumerDurableNameNotSetErr)
 
 	// Now delete a msg.
 	dreq := JSApiMsgDeleteRequest{Seq: 2}
@@ -7804,7 +7783,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
 	if err = json.Unmarshal(resp.Data, &stResp); err != nil {
 		t.Fatalf("Unexpected error: %v", err)
 	}
-	checkBadRequest(stResp.Error, "template name in subject does not match request")
+	checkNatsError(t, stResp.Error, JSTemplateNameNotMatchSubjectErr)
 
 	resp, _ = nc.Request(fmt.Sprintf(JSApiTemplateCreateT, template.Name), req, time.Second)
 	stResp.Error, stResp.StreamTemplateInfo = nil, nil
@@ -7857,7 +7836,7 @@ func TestJetStreamRequestAPI(t *testing.T) {
 	if err = json.Unmarshal(resp.Data, &tDeleteResp); err != nil {
 		t.Fatalf("Unexpected error: %v", err)
 	}
-	checkServerError(tDeleteResp.Error, "template not found")
+	checkNatsError(t, tDeleteResp.Error, JSStreamTemplateNotFoundErr)
 
 	resp, _ = nc.Request(fmt.Sprintf(JSApiTemplateDeleteT, "ss"), nil, time.Second)
 	tDeleteResp.Error = nil
diff --git a/server/test_test.go b/server/test_test.go
index c7cc3004..6bdaf905 100644
--- a/server/test_test.go
+++ b/server/test_test.go
@@ -58,6 +58,18 @@ type cluster struct {
 	t       *testing.T
 }
 
+func checkNatsError(t *testing.T, e *ApiError, id ErrorIdentifier) {
+	t.Helper()
+	ae, ok := ApiErrors[id]
+	if !ok {
+		t.Fatalf("Unknown error ID identifier: %d", id)
+	}
+
+	if e.ErrCode != ae.ErrCode {
+		t.Fatalf("Did not get NATS Error %d: %+v", e.ErrCode, e)
+	}
+}
+
 // Creates a full cluster with numServers and given name and makes sure its well formed.
 // Will have Gateways and Leaf Node connections active.
 func createClusterWithName(t *testing.T, clusterName string, numServers int, connectTo ...*cluster) *cluster {