diff --git a/Cargo.lock b/Cargo.lock
index 079f12c3ba9..a6a3567b079 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2110,8 +2110,10 @@ dependencies = [
  "near-network 0.1.0",
  "near-pool 0.1.0",
  "near-primitives 0.1.0",
+ "near-rpc-error-macro 0.1.0",
  "near-store 0.1.0",
  "prometheus 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2200,6 +2202,8 @@ dependencies = [
  "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "near-crypto 0.1.0",
+ "near-rpc-error-macro 0.1.0",
+ "near-vm-errors 0.4.3",
  "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "reed-solomon-erasure 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2209,6 +2213,29 @@ dependencies = [
  "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "near-rpc-error-core"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "near-rpc-error-macro"
+version = "0.1.0"
+dependencies = [
+ "near-rpc-error-core 0.1.0",
+ "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "near-runtime-fees"
 version = "0.4.3"
@@ -2252,6 +2279,11 @@ dependencies = [
 [[package]]
 name = "near-vm-errors"
 version = "0.4.3"
+dependencies = [
+ "borsh 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "near-rpc-error-macro 0.1.0",
+ "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
+]
 
 [[package]]
 name = "near-vm-logic"
@@ -3468,6 +3500,7 @@ dependencies = [
  "near-primitives 0.1.0",
  "near-runtime-fees 0.4.3",
  "near-store 0.1.0",
+ "near-vm-errors 0.4.3",
  "node-runtime 0.0.1",
  "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/Cargo.toml b/Cargo.toml
index 5356e1c0bb5..62f6b37e096 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,8 @@ members = [
     "test-utils/loadtester",
     "test-utils/state-viewer",
     "near/",
+    "tools/rpctypegen/core",
+    "tools/rpctypegen/macro",
     "genesis-tools/genesis-csv-to-json",
     "genesis-tools/genesis-populate",
     "genesis-tools/keypair-generator",
diff --git a/chain/jsonrpc/Cargo.toml b/chain/jsonrpc/Cargo.toml
index 0f050ded1e4..7c46872ed2c 100644
--- a/chain/jsonrpc/Cargo.toml
+++ b/chain/jsonrpc/Cargo.toml
@@ -21,7 +21,6 @@ serde_derive = "1.0"
 serde_json = "1.0"
 serde = { version = "1.0", features = ["derive"] }
 uuid = { version = "~0.8", features = ["v4"] }
-
 borsh = "0.2.10"
 
 near-crypto = { path = "../../core/crypto" }
@@ -33,3 +32,10 @@ near-client = { path = "../client" }
 near-network = { path = "../network" }
 near-pool = { path = "../pool" }
 near-jsonrpc-client = { path = "client" }
+near-rpc-error-macro = { path = "../../tools/rpctypegen/macro" }
+
+[build-dependencies]
+_rand = { package = "rand", version = "0.6.5" }
+
+[features]
+dump_errors_schema = ["near-rpc-error-macro/dump_errors_schema"]
diff --git a/chain/jsonrpc/build_errors_schema.sh b/chain/jsonrpc/build_errors_schema.sh
new file mode 100755
index 00000000000..59aa28ddf35
--- /dev/null
+++ b/chain/jsonrpc/build_errors_schema.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+cargo build --features dump_errors_schema
+cp ../../target/rpc_errors_schema.json ./res/rpc_errors_schema.json
diff --git a/chain/jsonrpc/res/rpc_errors_schema.json b/chain/jsonrpc/res/rpc_errors_schema.json
new file mode 100644
index 00000000000..1b97a6b787c
--- /dev/null
+++ b/chain/jsonrpc/res/rpc_errors_schema.json
@@ -0,0 +1,562 @@
+{
+  "schema": {
+    "BadUTF16": {
+      "name": "BadUTF16",
+      "subtypes": [],
+      "props": {}
+    },
+    "BadUTF8": {
+      "name": "BadUTF8",
+      "subtypes": [],
+      "props": {}
+    },
+    "BalanceExceeded": {
+      "name": "BalanceExceeded",
+      "subtypes": [],
+      "props": {}
+    },
+    "CannotAppendActionToJointPromise": {
+      "name": "CannotAppendActionToJointPromise",
+      "subtypes": [],
+      "props": {}
+    },
+    "CannotReturnJointPromise": {
+      "name": "CannotReturnJointPromise",
+      "subtypes": [],
+      "props": {}
+    },
+    "CodeDoesNotExist": {
+      "name": "CodeDoesNotExist",
+      "subtypes": [],
+      "props": {
+        "account_id": ""
+      }
+    },
+    "CompilationError": {
+      "name": "CompilationError",
+      "subtypes": [
+        "CodeDoesNotExist",
+        "PrepareError",
+        "WasmerCompileError"
+      ],
+      "props": {}
+    },
+    "Deserialization": {
+      "name": "Deserialization",
+      "subtypes": [],
+      "props": {}
+    },
+    "EmptyMethodName": {
+      "name": "EmptyMethodName",
+      "subtypes": [],
+      "props": {}
+    },
+    "FunctionExecError": {
+      "name": "FunctionExecError",
+      "subtypes": [
+        "CompilationError",
+        "LinkError",
+        "MethodResolveError",
+        "WasmTrap",
+        "HostError"
+      ],
+      "props": {}
+    },
+    "GasExceeded": {
+      "name": "GasExceeded",
+      "subtypes": [],
+      "props": {}
+    },
+    "GasInstrumentation": {
+      "name": "GasInstrumentation",
+      "subtypes": [],
+      "props": {}
+    },
+    "GasLimitExceeded": {
+      "name": "GasLimitExceeded",
+      "subtypes": [],
+      "props": {}
+    },
+    "GuestPanic": {
+      "name": "GuestPanic",
+      "subtypes": [],
+      "props": {
+        "panic_msg": ""
+      }
+    },
+    "HostError": {
+      "name": "HostError",
+      "subtypes": [
+        "BadUTF16",
+        "BadUTF8",
+        "GasExceeded",
+        "GasLimitExceeded",
+        "BalanceExceeded",
+        "EmptyMethodName",
+        "GuestPanic",
+        "IntegerOverflow",
+        "InvalidPromiseIndex",
+        "CannotAppendActionToJointPromise",
+        "CannotReturnJointPromise",
+        "InvalidPromiseResultIndex",
+        "InvalidRegisterId",
+        "IteratorWasInvalidated",
+        "MemoryAccessViolation",
+        "InvalidReceiptIndex",
+        "InvalidIteratorIndex",
+        "InvalidAccountId",
+        "InvalidMethodName",
+        "InvalidPublicKey",
+        "ProhibitedInView"
+      ],
+      "props": {}
+    },
+    "Instantiate": {
+      "name": "Instantiate",
+      "subtypes": [],
+      "props": {}
+    },
+    "IntegerOverflow": {
+      "name": "IntegerOverflow",
+      "subtypes": [],
+      "props": {}
+    },
+    "InternalMemoryDeclared": {
+      "name": "InternalMemoryDeclared",
+      "subtypes": [],
+      "props": {}
+    },
+    "InvalidAccountId": {
+      "name": "InvalidAccountId",
+      "subtypes": [],
+      "props": {}
+    },
+    "InvalidIteratorIndex": {
+      "name": "InvalidIteratorIndex",
+      "subtypes": [],
+      "props": {
+        "iterator_index": ""
+      }
+    },
+    "InvalidMethodName": {
+      "name": "InvalidMethodName",
+      "subtypes": [],
+      "props": {}
+    },
+    "InvalidPromiseIndex": {
+      "name": "InvalidPromiseIndex",
+      "subtypes": [],
+      "props": {
+        "promise_idx": ""
+      }
+    },
+    "InvalidPromiseResultIndex": {
+      "name": "InvalidPromiseResultIndex",
+      "subtypes": [],
+      "props": {
+        "result_idx": ""
+      }
+    },
+    "InvalidPublicKey": {
+      "name": "InvalidPublicKey",
+      "subtypes": [],
+      "props": {}
+    },
+    "InvalidReceiptIndex": {
+      "name": "InvalidReceiptIndex",
+      "subtypes": [],
+      "props": {
+        "receipt_index": ""
+      }
+    },
+    "InvalidRegisterId": {
+      "name": "InvalidRegisterId",
+      "subtypes": [],
+      "props": {
+        "register_id": ""
+      }
+    },
+    "IteratorWasInvalidated": {
+      "name": "IteratorWasInvalidated",
+      "subtypes": [],
+      "props": {
+        "iterator_index": ""
+      }
+    },
+    "LinkError": {
+      "name": "LinkError",
+      "subtypes": [],
+      "props": {
+        "msg": ""
+      }
+    },
+    "Memory": {
+      "name": "Memory",
+      "subtypes": [],
+      "props": {}
+    },
+    "MemoryAccessViolation": {
+      "name": "MemoryAccessViolation",
+      "subtypes": [],
+      "props": {}
+    },
+    "MethodEmptyName": {
+      "name": "MethodEmptyName",
+      "subtypes": [],
+      "props": {}
+    },
+    "MethodInvalidSignature": {
+      "name": "MethodInvalidSignature",
+      "subtypes": [],
+      "props": {}
+    },
+    "MethodNotFound": {
+      "name": "MethodNotFound",
+      "subtypes": [],
+      "props": {}
+    },
+    "MethodResolveError": {
+      "name": "MethodResolveError",
+      "subtypes": [
+        "MethodEmptyName",
+        "MethodUTF8Error",
+        "MethodNotFound",
+        "MethodInvalidSignature"
+      ],
+      "props": {}
+    },
+    "MethodUTF8Error": {
+      "name": "MethodUTF8Error",
+      "subtypes": [],
+      "props": {}
+    },
+    "PrepareError": {
+      "name": "PrepareError",
+      "subtypes": [
+        "Serialization",
+        "Deserialization",
+        "InternalMemoryDeclared",
+        "GasInstrumentation",
+        "StackHeightInstrumentation",
+        "Instantiate",
+        "Memory"
+      ],
+      "props": {}
+    },
+    "ProhibitedInView": {
+      "name": "ProhibitedInView",
+      "subtypes": [],
+      "props": {
+        "method_name": ""
+      }
+    },
+    "Serialization": {
+      "name": "Serialization",
+      "subtypes": [],
+      "props": {}
+    },
+    "StackHeightInstrumentation": {
+      "name": "StackHeightInstrumentation",
+      "subtypes": [],
+      "props": {}
+    },
+    "VMError": {
+      "name": "VMError",
+      "subtypes": [
+        "FunctionExecError",
+        "StorageError"
+      ],
+      "props": {}
+    },
+    "WasmTrap": {
+      "name": "WasmTrap",
+      "subtypes": [],
+      "props": {
+        "msg": ""
+      }
+    },
+    "WasmerCompileError": {
+      "name": "WasmerCompileError",
+      "subtypes": [],
+      "props": {
+        "msg": ""
+      }
+    },
+    "AccessKeyNotFound": {
+      "name": "AccessKeyNotFound",
+      "subtypes": [],
+      "props": {
+        "account_id": "",
+        "public_key": ""
+      }
+    },
+    "AccountAlreadyExists": {
+      "name": "AccountAlreadyExists",
+      "subtypes": [],
+      "props": {
+        "account_id": ""
+      }
+    },
+    "AccountDoesNotExist": {
+      "name": "AccountDoesNotExist",
+      "subtypes": [],
+      "props": {
+        "account_id": ""
+      }
+    },
+    "ActionError": {
+      "name": "ActionError",
+      "subtypes": [
+        "AccountAlreadyExists",
+        "AccountDoesNotExist",
+        "CreateAccountNotAllowed",
+        "ActorNoPermission",
+        "DeleteKeyDoesNotExist",
+        "AddKeyAlreadyExists",
+        "DeleteAccountStaking",
+        "DeleteAccountHasRent",
+        "RentUnpaid",
+        "TriesToUnstake",
+        "TriesToStake",
+        "FunctionCall"
+      ],
+      "props": {
+        "index": ""
+      }
+    },
+    "ActorNoPermission": {
+      "name": "ActorNoPermission",
+      "subtypes": [],
+      "props": {
+        "account_id": "",
+        "actor_id": ""
+      }
+    },
+    "AddKeyAlreadyExists": {
+      "name": "AddKeyAlreadyExists",
+      "subtypes": [],
+      "props": {
+        "account_id": "",
+        "public_key": ""
+      }
+    },
+    "BalanceMismatchError": {
+      "name": "BalanceMismatchError",
+      "subtypes": [],
+      "props": {
+        "final_accounts_balance": "",
+        "final_postponed_receipts_balance": "",
+        "incoming_receipts_balance": "",
+        "incoming_validator_rewards": "",
+        "initial_accounts_balance": "",
+        "initial_postponed_receipts_balance": "",
+        "new_delayed_receipts_balance": "",
+        "outgoing_receipts_balance": "",
+        "processed_delayed_receipts_balance": "",
+        "total_balance_burnt": "",
+        "total_balance_slashed": "",
+        "total_rent_paid": "",
+        "total_validator_reward": ""
+      }
+    },
+    "CostOverflow": {
+      "name": "CostOverflow",
+      "subtypes": [],
+      "props": {}
+    },
+    "CreateAccountNotAllowed": {
+      "name": "CreateAccountNotAllowed",
+      "subtypes": [],
+      "props": {
+        "account_id": "",
+        "predecessor_id": ""
+      }
+    },
+    "DeleteAccountHasRent": {
+      "name": "DeleteAccountHasRent",
+      "subtypes": [],
+      "props": {
+        "account_id": "",
+        "balance": ""
+      }
+    },
+    "DeleteAccountStaking": {
+      "name": "DeleteAccountStaking",
+      "subtypes": [],
+      "props": {
+        "account_id": ""
+      }
+    },
+    "DeleteKeyDoesNotExist": {
+      "name": "DeleteKeyDoesNotExist",
+      "subtypes": [],
+      "props": {
+        "account_id": "",
+        "public_key": ""
+      }
+    },
+    "Expired": {
+      "name": "Expired",
+      "subtypes": [],
+      "props": {}
+    },
+    "InvalidAccessKeyError": {
+      "name": "InvalidAccessKeyError",
+      "subtypes": [
+        "AccessKeyNotFound",
+        "ReceiverMismatch",
+        "MethodNameMismatch",
+        "RequiresFullAccess",
+        "NotEnoughAllowance"
+      ],
+      "props": {}
+    },
+    "InvalidChain": {
+      "name": "InvalidChain",
+      "subtypes": [],
+      "props": {}
+    },
+    "InvalidNonce": {
+      "name": "InvalidNonce",
+      "subtypes": [],
+      "props": {
+        "ak_nonce": "",
+        "tx_nonce": ""
+      }
+    },
+    "InvalidReceiverId": {
+      "name": "InvalidReceiverId",
+      "subtypes": [],
+      "props": {
+        "receiver_id": ""
+      }
+    },
+    "InvalidSignature": {
+      "name": "InvalidSignature",
+      "subtypes": [],
+      "props": {}
+    },
+    "InvalidSignerId": {
+      "name": "InvalidSignerId",
+      "subtypes": [],
+      "props": {
+        "signer_id": ""
+      }
+    },
+    "InvalidTxError": {
+      "name": "InvalidTxError",
+      "subtypes": [
+        "InvalidAccessKeyError",
+        "InvalidSignerId",
+        "SignerDoesNotExist",
+        "InvalidNonce",
+        "InvalidReceiverId",
+        "InvalidSignature",
+        "NotEnoughBalance",
+        "RentUnpaid",
+        "CostOverflow",
+        "InvalidChain",
+        "Expired"
+      ],
+      "props": {}
+    },
+    "MethodNameMismatch": {
+      "name": "MethodNameMismatch",
+      "subtypes": [],
+      "props": {
+        "method_name": ""
+      }
+    },
+    "NotEnoughAllowance": {
+      "name": "NotEnoughAllowance",
+      "subtypes": [],
+      "props": {
+        "account_id": "",
+        "allowance": "",
+        "cost": "",
+        "public_key": ""
+      }
+    },
+    "NotEnoughBalance": {
+      "name": "NotEnoughBalance",
+      "subtypes": [],
+      "props": {
+        "balance": "",
+        "cost": "",
+        "signer_id": ""
+      }
+    },
+    "ReceiverMismatch": {
+      "name": "ReceiverMismatch",
+      "subtypes": [],
+      "props": {
+        "ak_receiver": "",
+        "tx_receiver": ""
+      }
+    },
+    "RentUnpaid": {
+      "name": "RentUnpaid",
+      "subtypes": [],
+      "props": {
+        "account_id": "",
+        "amount": ""
+      }
+    },
+    "SignerDoesNotExist": {
+      "name": "SignerDoesNotExist",
+      "subtypes": [],
+      "props": {
+        "signer_id": ""
+      }
+    },
+    "TriesToStake": {
+      "name": "TriesToStake",
+      "subtypes": [],
+      "props": {
+        "account_id": "",
+        "balance": "",
+        "locked": "",
+        "stake": ""
+      }
+    },
+    "TriesToUnstake": {
+      "name": "TriesToUnstake",
+      "subtypes": [],
+      "props": {
+        "account_id": ""
+      }
+    },
+    "TxExecutionError": {
+      "name": "TxExecutionError",
+      "subtypes": [
+        "ActionError",
+        "InvalidTxError"
+      ],
+      "props": {}
+    },
+    "Closed": {
+      "name": "Closed",
+      "subtypes": [],
+      "props": {}
+    },
+    "ServerError": {
+      "name": "ServerError",
+      "subtypes": [
+        "TxExecutionError",
+        "Timeout",
+        "Closed"
+      ],
+      "props": {}
+    },
+    "Timeout": {
+      "name": "Timeout",
+      "subtypes": [],
+      "props": {}
+    },
+    "RequiresFullAccess": {
+      "name": "RequiresFullAccess",
+      "subtypes": [],
+      "props": {}
+    }
+  }
+}
\ No newline at end of file
diff --git a/chain/jsonrpc/src/lib.rs b/chain/jsonrpc/src/lib.rs
index 376bb3af24d..6ea0202aa81 100644
--- a/chain/jsonrpc/src/lib.rs
+++ b/chain/jsonrpc/src/lib.rs
@@ -1,6 +1,7 @@
 extern crate prometheus;
 
 use std::convert::TryFrom;
+use std::fmt::Display;
 use std::string::FromUtf8Error;
 use std::time::Duration;
 
@@ -25,11 +26,12 @@ pub use near_jsonrpc_client as client;
 use near_jsonrpc_client::{message, ChunkId};
 use near_metrics::{Encoder, TextEncoder};
 use near_network::{NetworkClientMessages, NetworkClientResponses};
+use near_primitives::errors::{InvalidTxError, TxExecutionError};
 use near_primitives::hash::CryptoHash;
 use near_primitives::serialize::{from_base, from_base64, BaseEncode};
 use near_primitives::transaction::SignedTransaction;
 use near_primitives::types::{AccountId, BlockId, MaybeBlockId, StateChangesRequest};
-use near_primitives::views::{ExecutionErrorView, FinalExecutionStatus};
+use near_primitives::views::FinalExecutionStatus;
 
 mod metrics;
 pub mod test_utils;
@@ -111,15 +113,47 @@ fn parse_tx(params: Option<Value>) -> Result<SignedTransaction, RpcError> {
         .map_err(|e| RpcError::invalid_params(Some(format!("Failed to decode transaction: {}", e))))
 }
 
-fn convert_mailbox_error(e: MailboxError) -> ExecutionErrorView {
-    ExecutionErrorView { error_message: e.to_string(), error_type: "MailBoxError".to_string() }
+/// A general Server Error
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, near_rpc_error_macro::RpcError)]
+pub enum ServerError {
+    TxExecutionError(TxExecutionError),
+    Timeout,
+    Closed,
+}
+
+impl Display for ServerError {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+        match self {
+            ServerError::TxExecutionError(e) => write!(f, "ServerError: {}", e),
+            ServerError::Timeout => write!(f, "ServerError: Timeout"),
+            ServerError::Closed => write!(f, "ServerError: Closed"),
+        }
+    }
+}
+
+impl From<InvalidTxError> for ServerError {
+    fn from(e: InvalidTxError) -> ServerError {
+        ServerError::TxExecutionError(TxExecutionError::InvalidTxError(e))
+    }
+}
+
+impl From<MailboxError> for ServerError {
+    fn from(e: MailboxError) -> Self {
+        match e {
+            MailboxError::Closed => ServerError::Closed,
+            MailboxError::Timeout => ServerError::Timeout,
+        }
+    }
+}
+
+impl From<ServerError> for RpcError {
+    fn from(e: ServerError) -> RpcError {
+        RpcError::server_error(Some(e))
+    }
 }
 
 fn timeout_err() -> RpcError {
-    RpcError::server_error(Some(ExecutionErrorView {
-        error_message: "send_tx_commit has timed out".to_string(),
-        error_type: "TimeoutError".to_string(),
-    }))
+    RpcError::server_error(Some(ServerError::Timeout))
 }
 
 struct JsonRpcHandler {
@@ -199,20 +233,17 @@ impl JsonRpcHandler {
         let result = self
             .client_addr
             .send(NetworkClientMessages::Transaction(tx))
-            .map_err(|err| RpcError::server_error(Some(convert_mailbox_error(err))))
+            .map_err(|err| RpcError::server_error(Some(ServerError::from(err))))
             .await?;
         match result {
             NetworkClientResponses::ValidTx | NetworkClientResponses::RequestRouted => {
                 self.tx_polling(tx_hash, signer_account_id).await
             }
             NetworkClientResponses::InvalidTx(err) => {
-                Err(RpcError::server_error(Some(ExecutionErrorView::from(err))))
+                Err(RpcError::server_error(Some(ServerError::TxExecutionError(err.into()))))
             }
             NetworkClientResponses::NoResponse => {
-                Err(RpcError::server_error(Some(ExecutionErrorView {
-                    error_message: "send_tx_commit has timed out".to_string(),
-                    error_type: "TimeoutError".to_string(),
-                })))
+                Err(RpcError::server_error(Some(ServerError::Timeout)))
             }
             _ => unreachable!(),
         }
diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml
index aaa66b8f0a9..afe5e1077fd 100644
--- a/core/primitives/Cargo.toml
+++ b/core/primitives/Cargo.toml
@@ -26,9 +26,12 @@ actix = "0.9.0"
 borsh = "0.2.10"
 
 near-crypto = { path = "../crypto" }
+near-vm-errors = { path = "../../runtime/near-vm-errors" }
+near-rpc-error-macro = { path = "../../tools/rpctypegen/macro" }
 
 [features]
 default = ["jemallocator"]
+dump_errors_schema = ["near-rpc-error-macro/dump_errors_schema"]
 
 [dev-dependencies]
 bencher = "0.1.5"
diff --git a/core/primitives/src/errors.rs b/core/primitives/src/errors.rs
index 2ad2b309b7a..679da83c88b 100644
--- a/core/primitives/src/errors.rs
+++ b/core/primitives/src/errors.rs
@@ -1,10 +1,62 @@
+use crate::serialize::u128_dec_format;
 use crate::types::{AccountId, Balance, Nonce};
 use borsh::{BorshDeserialize, BorshSerialize};
 use near_crypto::PublicKey;
+use serde::{Deserialize, Serialize};
 use std::fmt::Display;
 
+use near_rpc_error_macro::RpcError;
+use near_vm_errors::VMError;
+
+/// Error returned in the ExecutionOutcome in case of failure
+#[derive(
+    BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError,
+)]
+pub enum TxExecutionError {
+    /// An error happened during Acton execution
+    ActionError(ActionError),
+    /// An error happened during Transaction execution
+    InvalidTxError(InvalidTxError),
+}
+
+impl Display for TxExecutionError {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+        match self {
+            TxExecutionError::ActionError(e) => write!(f, "{}", e),
+            TxExecutionError::InvalidTxError(e) => write!(f, "{}", e),
+        }
+    }
+}
+
+impl From<ActionError> for TxExecutionError {
+    fn from(error: ActionError) -> Self {
+        TxExecutionError::ActionError(error)
+    }
+}
+
+impl From<InvalidTxError> for TxExecutionError {
+    fn from(error: InvalidTxError) -> Self {
+        TxExecutionError::InvalidTxError(error)
+    }
+}
+
+/// Error returned from `Runtime::apply`
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RuntimeError {
+    /// An unexpected integer overflow occurred. The likely issue is an invalid state or the transition.
+    UnexpectedIntegerOverflow,
+    /// An error happened during TX verification and account charging. It's likely the chunk is invalid.
+    /// and should be challenged.
+    InvalidTxError(InvalidTxError),
+    /// Unexpected error which is typically related to the node storage corruption.account
+    /// That it's possible the input state is invalid or malicious.
+    StorageError(StorageError),
+    /// An error happens if `check_balance` fails, which is likely an indication of an invalid state.
+    BalanceMismatchError(BalanceMismatchError),
+}
+
 /// Internal
-#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)]
+#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq)]
 pub enum StorageError {
     /// Key-value db internal failure
     StorageInternalError,
@@ -26,74 +78,164 @@ impl std::fmt::Display for StorageError {
 
 impl std::error::Error for StorageError {}
 
-/// External
-#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)]
+/// An error happened during TX execution
+#[derive(
+    BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError,
+)]
 pub enum InvalidTxError {
-    InvalidSigner(AccountId),
-    SignerDoesNotExist(AccountId),
-    InvalidAccessKey(InvalidAccessKeyError),
-    InvalidNonce(Nonce, Nonce),
-    InvalidReceiver(AccountId),
+    /// Happens if a wrong AccessKey used or AccessKey has not enough permissions
+    InvalidAccessKeyError(InvalidAccessKeyError),
+    /// TX signer_id is not in a valid format or not satisfy requirements see `near_core::primitives::utils::is_valid_account_id`
+    InvalidSignerId { signer_id: AccountId },
+    /// TX signer_id is not found in a storage
+    SignerDoesNotExist { signer_id: AccountId },
+    /// Transaction nonce must be account[access_key].nonce + 1
+    InvalidNonce { tx_nonce: Nonce, ak_nonce: Nonce },
+    /// TX receiver_id is not in a valid format or not satisfy requirements see `near_core::primitives::utils::is_valid_account_id`
+    InvalidReceiverId { receiver_id: AccountId },
+    /// TX signature is not valid
     InvalidSignature,
-    NotEnoughBalance(AccountId, Balance, Balance),
-    RentUnpaid(AccountId, Balance),
+    /// Account does not have enough balance to cover TX cost
+    NotEnoughBalance {
+        signer_id: AccountId,
+        #[serde(with = "u128_dec_format")]
+        balance: Balance,
+        #[serde(with = "u128_dec_format")]
+        cost: Balance,
+    },
+    /// Signer account rent is unpaid
+    RentUnpaid {
+        /// An account which is required to pay the rent
+        signer_id: AccountId,
+        /// Required balance to cover the state rent
+        #[serde(with = "u128_dec_format")]
+        amount: Balance,
+    },
+    /// An integer overflow occurred during transaction cost estimation.
     CostOverflow,
+    /// Transaction parent block hash doesn't belong to the current chain
     InvalidChain,
+    /// Transaction has expired
     Expired,
 }
 
-#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)]
+#[derive(
+    BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError,
+)]
 pub enum InvalidAccessKeyError {
-    AccessKeyNotFound(AccountId, PublicKey),
-    ReceiverMismatch(AccountId, AccountId),
-    MethodNameMismatch(String),
-    ActionError,
-    NotEnoughAllowance(AccountId, PublicKey, Balance, Balance),
+    /// The access key identified by the `public_key` doesn't exist for the account
+    AccessKeyNotFound { account_id: AccountId, public_key: PublicKey },
+    /// Transaction `receiver_id` doesn't match the access key receiver_id
+    ReceiverMismatch { tx_receiver: AccountId, ak_receiver: AccountId },
+    /// Transaction method name isn't allowed by the access key
+    MethodNameMismatch { method_name: String },
+    /// Transaction requires a full permission access key.
+    RequiresFullAccess,
+    /// Access Key does not have enough allowance to cover transaction cost
+    NotEnoughAllowance {
+        account_id: AccountId,
+        public_key: PublicKey,
+        #[serde(with = "u128_dec_format")]
+        allowance: Balance,
+        #[serde(with = "u128_dec_format")]
+        cost: Balance,
+    },
 }
 
-#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)]
-pub enum ActionError {
-    AccountAlreadyExists(AccountId),
-    AccountDoesNotExist(String, AccountId),
-    CreateAccountNotAllowed(AccountId, AccountId),
-    ActorNoPermission(AccountId, AccountId, String),
-    DeleteKeyDoesNotExist(AccountId),
-    AddKeyAlreadyExists(PublicKey),
-    DeleteAccountStaking(AccountId),
-    DeleteAccountHasRent(AccountId, Balance),
-    RentUnpaid(AccountId, Balance),
-    TriesToUnstake(AccountId),
-    TriesToStake(AccountId, Balance, Balance, Balance),
-    FunctionCallError(String), // TODO type
+/// An error happened during Acton execution
+#[derive(
+    BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError,
+)]
+pub struct ActionError {
+    /// Index of the failed action in the transaction.
+    /// Action index is not defined if ActionError.kind is `ActionErrorKind::RentUnpaid`
+    pub index: Option<u64>,
+    /// The kind of ActionError happened
+    pub kind: ActionErrorKind,
+}
+
+#[derive(
+    BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError,
+)]
+pub enum ActionErrorKind {
+    /// Happens when CreateAccount action tries to create an account with account_id which is already exists in the storage
+    AccountAlreadyExists { account_id: AccountId },
+    /// Happens when TX receiver_id doesn't exist (but action is not Action::CreateAccount)
+    AccountDoesNotExist { account_id: AccountId },
+    /// A newly created account must be under a namespace of the creator account
+    CreateAccountNotAllowed { account_id: AccountId, predecessor_id: AccountId },
+    /// Administrative actions like `DeployContract`, `Stake`, `AddKey`, `DeleteKey`. can be proceed only if sender=receiver
+    /// or the first TX action is a `CreateAccount` action
+    ActorNoPermission { account_id: AccountId, actor_id: AccountId },
+    /// Account tries to remove an access key that doesn't exist
+    DeleteKeyDoesNotExist { account_id: AccountId, public_key: PublicKey },
+    /// The public key is already used for an existing access key
+    AddKeyAlreadyExists { account_id: AccountId, public_key: PublicKey },
+    /// Account is staking and can not be deleted
+    DeleteAccountStaking { account_id: AccountId },
+    /// Foreign sender (sender=!receiver) can delete an account only if a target account hasn't enough tokens to pay rent
+    DeleteAccountHasRent {
+        account_id: AccountId,
+        #[serde(with = "u128_dec_format")]
+        balance: Balance,
+    },
+    /// ActionReceipt can't be completed, because the remaining balance will not be enough to pay rent.
+    RentUnpaid {
+        /// An account which is required to pay the rent
+        account_id: AccountId,
+        /// Rent due to pay.
+        #[serde(with = "u128_dec_format")]
+        amount: Balance,
+    },
+    /// Account is not yet staked, but tries to unstake
+    TriesToUnstake { account_id: AccountId },
+    /// The account doesn't have enough balance to increase the stake.
+    TriesToStake {
+        account_id: AccountId,
+        #[serde(with = "u128_dec_format")]
+        stake: Balance,
+        #[serde(with = "u128_dec_format")]
+        locked: Balance,
+        #[serde(with = "u128_dec_format")]
+        balance: Balance,
+    },
+    /// An error occurred during a `FunctionCall` Action.
+    FunctionCall(VMError),
+}
+
+impl From<ActionErrorKind> for ActionError {
+    fn from(e: ActionErrorKind) -> ActionError {
+        ActionError { index: None, kind: e }
+    }
 }
 
 impl Display for InvalidTxError {
     fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
         match self {
-            InvalidTxError::InvalidSigner(signer_id) => {
+            InvalidTxError::InvalidSignerId{signer_id} => {
                 write!(f, "Invalid signer account ID {:?} according to requirements", signer_id)
             }
-            InvalidTxError::SignerDoesNotExist(signer_id) => {
+            InvalidTxError::SignerDoesNotExist{signer_id} => {
                 write!(f, "Signer {:?} does not exist", signer_id)
             }
-            InvalidTxError::InvalidAccessKey(access_key_error) => access_key_error.fmt(f),
-            InvalidTxError::InvalidNonce(tx_nonce, ak_nonce) => write!(
+            InvalidTxError::InvalidAccessKeyError(access_key_error) => access_key_error.fmt(f),
+            InvalidTxError::InvalidNonce{tx_nonce, ak_nonce} => write!(
                 f,
                 "Transaction nonce {} must be larger than nonce of the used access key {}",
                 tx_nonce, ak_nonce
             ),
-            InvalidTxError::InvalidReceiver(receiver_id) => {
+            InvalidTxError::InvalidReceiverId{receiver_id} => {
                 write!(f, "Invalid receiver account ID {:?} according to requirements", receiver_id)
             }
             InvalidTxError::InvalidSignature => {
                 write!(f, "Transaction is not signed with the given public key")
             }
-            InvalidTxError::NotEnoughBalance(signer_id, balance, cost) => write!(
+            InvalidTxError::NotEnoughBalance{signer_id, balance, cost} => write!(
                 f,
                 "Sender {:?} does not have enough balance {} for operation costing {}",
                 signer_id, balance, cost
             ),
-            InvalidTxError::RentUnpaid(signer_id, amount) => {
+            InvalidTxError::RentUnpaid{ signer_id, amount} => {
                 write!(f, "Failed to execute, because the account {:?} wouldn't have enough to pay required rent {}", signer_id, amount)
             }
             InvalidTxError::CostOverflow => {
@@ -111,59 +253,80 @@ impl Display for InvalidTxError {
 
 impl From<InvalidAccessKeyError> for InvalidTxError {
     fn from(error: InvalidAccessKeyError) -> Self {
-        InvalidTxError::InvalidAccessKey(error)
+        InvalidTxError::InvalidAccessKeyError(error)
     }
 }
 
 impl Display for InvalidAccessKeyError {
     fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
         match self {
-            InvalidAccessKeyError::AccessKeyNotFound(account_id, public_key) => write!(
+            InvalidAccessKeyError::AccessKeyNotFound { account_id, public_key } => write!(
                 f,
                 "Signer {:?} doesn't have access key with the given public_key {}",
                 account_id, public_key
             ),
-            InvalidAccessKeyError::ReceiverMismatch(tx_receiver, ak_receiver) => write!(
+            InvalidAccessKeyError::ReceiverMismatch { tx_receiver, ak_receiver } => write!(
                 f,
                 "Transaction receiver_id {:?} doesn't match the access key receiver_id {:?}",
                 tx_receiver, ak_receiver
             ),
-            InvalidAccessKeyError::MethodNameMismatch(method_name) => write!(
+            InvalidAccessKeyError::MethodNameMismatch { method_name } => write!(
                 f,
                 "Transaction method name {:?} isn't allowed by the access key",
                 method_name
             ),
-            InvalidAccessKeyError::ActionError => {
-                write!(f, "The used access key requires exactly one FunctionCall action")
-            }
-            InvalidAccessKeyError::NotEnoughAllowance(account_id, public_key, allowance, cost) => {
-                write!(
-                    f,
-                    "Access Key {:?}:{} does not have enough balance {} for transaction costing {}",
-                    account_id, public_key, allowance, cost
-                )
-            }
+            InvalidAccessKeyError::RequiresFullAccess => write!(
+                f,
+                "The transaction contains more then one action, but it was signed \
+                 with an access key which allows transaction to apply only one specific action. \
+                 To apply more then one actions TX must be signed with a full access key"
+            ),
+            InvalidAccessKeyError::NotEnoughAllowance {
+                account_id,
+                public_key,
+                allowance,
+                cost,
+            } => write!(
+                f,
+                "Access Key {:?}:{} does not have enough balance {} for transaction costing {}",
+                account_id, public_key, allowance, cost
+            ),
         }
     }
 }
 
 /// Happens when the input balance doesn't match the output balance in Runtime apply.
-#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)]
+#[derive(
+    BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError,
+)]
 pub struct BalanceMismatchError {
     // Input balances
+    #[serde(with = "u128_dec_format")]
     pub incoming_validator_rewards: Balance,
+    #[serde(with = "u128_dec_format")]
     pub initial_accounts_balance: Balance,
+    #[serde(with = "u128_dec_format")]
     pub incoming_receipts_balance: Balance,
+    #[serde(with = "u128_dec_format")]
     pub processed_delayed_receipts_balance: Balance,
+    #[serde(with = "u128_dec_format")]
     pub initial_postponed_receipts_balance: Balance,
     // Output balances
+    #[serde(with = "u128_dec_format")]
     pub final_accounts_balance: Balance,
+    #[serde(with = "u128_dec_format")]
     pub outgoing_receipts_balance: Balance,
+    #[serde(with = "u128_dec_format")]
     pub new_delayed_receipts_balance: Balance,
+    #[serde(with = "u128_dec_format")]
     pub final_postponed_receipts_balance: Balance,
+    #[serde(with = "u128_dec_format")]
     pub total_rent_paid: Balance,
+    #[serde(with = "u128_dec_format")]
     pub total_validator_reward: Balance,
+    #[serde(with = "u128_dec_format")]
     pub total_balance_burnt: Balance,
+    #[serde(with = "u128_dec_format")]
     pub total_balance_slashed: Balance,
 }
 
@@ -222,18 +385,9 @@ impl Display for BalanceMismatchError {
     }
 }
 
-#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)]
+#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq)]
 pub struct IntegerOverflowError;
 
-/// Error returned from `Runtime::apply`
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RuntimeError {
-    UnexpectedIntegerOverflow,
-    InvalidTxError(InvalidTxError),
-    StorageError(StorageError),
-    BalanceMismatch(BalanceMismatchError),
-}
-
 impl From<IntegerOverflowError> for InvalidTxError {
     fn from(_: IntegerOverflowError) -> Self {
         InvalidTxError::CostOverflow
@@ -254,7 +408,7 @@ impl From<StorageError> for RuntimeError {
 
 impl From<BalanceMismatchError> for RuntimeError {
     fn from(e: BalanceMismatchError) -> Self {
-        RuntimeError::BalanceMismatch(e)
+        RuntimeError::BalanceMismatchError(e)
     }
 }
 
@@ -265,86 +419,65 @@ impl From<InvalidTxError> for RuntimeError {
 }
 
 impl Display for ActionError {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
+        write!(f, "Action #{}: {}", self.index.unwrap_or_default(), self.kind)
+    }
+}
+
+impl Display for ActionErrorKind {
     fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
         match self {
-            ActionError::AccountAlreadyExists(account_id) => {
+            ActionErrorKind::AccountAlreadyExists { account_id } => {
                 write!(f, "Can't create a new account {:?}, because it already exists", account_id)
             }
-            ActionError::AccountDoesNotExist(action, account_id) => write!(
+
+            ActionErrorKind::AccountDoesNotExist { account_id } => write!(
                 f,
-                "Can't complete the action {:?}, because account {:?} doesn't exist",
-                action, account_id
+                "Can't complete the action because account {:?} doesn't exist",
+                account_id
             ),
-            ActionError::ActorNoPermission(actor_id, account_id, action) => write!(
+            ActionErrorKind::ActorNoPermission { actor_id, account_id } => write!(
                 f,
-                "Actor {:?} doesn't have permission to account {:?} to complete the action {:?}",
-                actor_id, account_id, action
+                "Actor {:?} doesn't have permission to account {:?} to complete the action",
+                actor_id, account_id
             ),
-            ActionError::RentUnpaid(account_id, amount) => write!(
+            ActionErrorKind::RentUnpaid { account_id, amount } => write!(
                 f,
                 "The account {} wouldn't have enough balance to pay required rent {}",
                 account_id, amount
             ),
-            ActionError::TriesToUnstake(account_id) => {
+            ActionErrorKind::TriesToUnstake { account_id } => {
                 write!(f, "Account {:?} is not yet staked, but tries to unstake", account_id)
             }
-            ActionError::TriesToStake(account_id, stake, staked, balance) => write!(
+            ActionErrorKind::TriesToStake { account_id, stake, locked, balance } => write!(
                 f,
                 "Account {:?} tries to stake {}, but has staked {} and only has {}",
-                account_id, stake, staked, balance
+                account_id, stake, locked, balance
             ),
-            ActionError::CreateAccountNotAllowed(account_id, predecessor_id) => write!(
+            ActionErrorKind::CreateAccountNotAllowed { account_id, predecessor_id } => write!(
                 f,
                 "The new account_id {:?} can't be created by {:?}",
                 account_id, predecessor_id
             ),
-            ActionError::DeleteKeyDoesNotExist(account_id) => write!(
+            ActionErrorKind::DeleteKeyDoesNotExist { account_id, .. } => write!(
                 f,
                 "Account {:?} tries to remove an access key that doesn't exist",
                 account_id
             ),
-            ActionError::AddKeyAlreadyExists(public_key) => write!(
+            ActionErrorKind::AddKeyAlreadyExists { public_key, .. } => write!(
                 f,
                 "The public key {:?} is already used for an existing access key",
                 public_key
             ),
-            ActionError::DeleteAccountStaking(account_id) => {
+            ActionErrorKind::DeleteAccountStaking { account_id } => {
                 write!(f, "Account {:?} is staking and can not be deleted", account_id)
             }
-            ActionError::DeleteAccountHasRent(account_id, balance) => write!(
+            ActionErrorKind::DeleteAccountHasRent { account_id, balance } => write!(
                 f,
                 "Account {:?} can't be deleted. It has {}, which is enough to cover the rent",
                 account_id, balance
             ),
-            ActionError::FunctionCallError(s) => write!(f, "{}", s),
+            ActionErrorKind::FunctionCall(s) => write!(f, "{}", s),
         }
     }
 }
-
-/// Error returned in the ExecutionOutcome in case of failure.
-#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)]
-pub enum ExecutionError {
-    Action(ActionError),
-    InvalidTx(InvalidTxError),
-}
-
-impl Display for ExecutionError {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
-        match self {
-            ExecutionError::Action(e) => write!(f, "{}", e),
-            ExecutionError::InvalidTx(e) => write!(f, "{}", e),
-        }
-    }
-}
-
-impl From<ActionError> for ExecutionError {
-    fn from(error: ActionError) -> Self {
-        ExecutionError::Action(error)
-    }
-}
-
-impl From<InvalidTxError> for ExecutionError {
-    fn from(error: InvalidTxError) -> Self {
-        ExecutionError::InvalidTx(error)
-    }
-}
diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs
index f7b674cea51..75861175cc0 100644
--- a/core/primitives/src/transaction.rs
+++ b/core/primitives/src/transaction.rs
@@ -6,7 +6,7 @@ use borsh::{BorshDeserialize, BorshSerialize};
 use near_crypto::{PublicKey, Signature, Signer};
 
 use crate::account::AccessKey;
-use crate::errors::ExecutionError;
+use crate::errors::TxExecutionError;
 use crate::hash::{hash, CryptoHash};
 use crate::logging;
 use crate::merkle::MerklePath;
@@ -211,7 +211,7 @@ pub enum ExecutionStatus {
     /// The execution is pending or unknown.
     Unknown,
     /// The execution has failed with the given execution error.
-    Failure(ExecutionError),
+    Failure(TxExecutionError),
     /// The final action succeeded and returned some value or an empty vec.
     SuccessValue(Vec<u8>),
     /// The final action of the receipt returned a promise or the signed transaction was converted
diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs
index 35c05ca4e0e..02d97c68088 100644
--- a/core/primitives/src/views.rs
+++ b/core/primitives/src/views.rs
@@ -10,7 +10,7 @@ use near_crypto::{PublicKey, Signature};
 use crate::account::{AccessKey, AccessKeyPermission, Account, FunctionCallPermission};
 use crate::block::{Approval, Block, BlockHeader, BlockHeaderInnerLite, BlockHeaderInnerRest};
 use crate::challenge::{Challenge, ChallengesResult};
-use crate::errors::{ActionError, ExecutionError, InvalidAccessKeyError, InvalidTxError};
+use crate::errors::TxExecutionError;
 use crate::hash::{hash, CryptoHash};
 use crate::logging;
 use crate::merkle::MerklePath;
@@ -650,7 +650,7 @@ pub enum FinalExecutionStatus {
     /// The execution has started and still going.
     Started,
     /// The execution has failed with the given error.
-    Failure(ExecutionErrorView),
+    Failure(TxExecutionError),
     /// The execution has succeeded and returned some value or an empty vec encoded in base64.
     SuccessValue(String),
 }
@@ -676,104 +676,10 @@ impl Default for FinalExecutionStatus {
 }
 
 #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
-pub struct ExecutionErrorView {
-    pub error_message: String,
-    pub error_type: String,
-}
-
-impl From<ExecutionError> for ExecutionErrorView {
-    fn from(error: ExecutionError) -> Self {
-        ExecutionErrorView {
-            error_message: format!("{}", error),
-            error_type: match &error {
-                ExecutionError::Action(e) => match e {
-                    ActionError::AccountAlreadyExists(_) => {
-                        "ActionError::AccountAlreadyExists".to_string()
-                    }
-                    ActionError::AccountDoesNotExist(_, _) => {
-                        "ActionError::AccountDoesNotExist".to_string()
-                    }
-                    ActionError::CreateAccountNotAllowed(_, _) => {
-                        "ActionError::CreateAccountNotAllowed".to_string()
-                    }
-                    ActionError::ActorNoPermission(_, _, _) => {
-                        "ActionError::ActorNoPermission".to_string()
-                    }
-                    ActionError::DeleteKeyDoesNotExist(_) => {
-                        "ActionError::DeleteKeyDoesNotExist".to_string()
-                    }
-                    ActionError::AddKeyAlreadyExists(_) => {
-                        "ActionError::AddKeyAlreadyExists".to_string()
-                    }
-                    ActionError::DeleteAccountStaking(_) => {
-                        "ActionError::DeleteAccountStaking".to_string()
-                    }
-                    ActionError::DeleteAccountHasRent(_, _) => {
-                        "ActionError::DeleteAccountHasRent".to_string()
-                    }
-                    ActionError::RentUnpaid(_, _) => "ActionError::RentUnpaid".to_string(),
-                    ActionError::TriesToUnstake(_) => "ActionError::TriesToUnstake".to_string(),
-                    ActionError::TriesToStake(_, _, _, _) => {
-                        "ActionError::TriesToStake".to_string()
-                    }
-                    ActionError::FunctionCallError(_) => {
-                        "ActionError::FunctionCallError".to_string()
-                    }
-                },
-                ExecutionError::InvalidTx(e) => match e {
-                    InvalidTxError::InvalidSigner(_) => "InvalidTxError::InvalidSigner".to_string(),
-                    InvalidTxError::SignerDoesNotExist(_) => {
-                        "InvalidTxError::SignerDoesNotExist".to_string()
-                    }
-                    InvalidTxError::InvalidAccessKey(e) => match e {
-                        InvalidAccessKeyError::AccessKeyNotFound(_, _) => {
-                            "InvalidTxError::InvalidAccessKey::AccessKeyNotFound".to_string()
-                        }
-                        InvalidAccessKeyError::ReceiverMismatch(_, _) => {
-                            "InvalidTxError::InvalidAccessKey::ReceiverMismatch".to_string()
-                        }
-                        InvalidAccessKeyError::MethodNameMismatch(_) => {
-                            "InvalidTxError::InvalidAccessKey::MethodNameMismatch".to_string()
-                        }
-                        InvalidAccessKeyError::ActionError => {
-                            "InvalidTxError::InvalidAccessKey::ActionError".to_string()
-                        }
-                        InvalidAccessKeyError::NotEnoughAllowance(_, _, _, _) => {
-                            "InvalidTxError::InvalidAccessKey::NotEnoughAllowance".to_string()
-                        }
-                    },
-                    InvalidTxError::InvalidNonce(_, _) => {
-                        "InvalidTxError::InvalidNonce".to_string()
-                    }
-                    InvalidTxError::InvalidReceiver(_) => {
-                        "InvalidTxError::InvalidReceiver".to_string()
-                    }
-                    InvalidTxError::InvalidSignature => {
-                        "InvalidTxError::InvalidSignature".to_string()
-                    }
-                    InvalidTxError::NotEnoughBalance(_, _, _) => {
-                        "InvalidTxError::NotEnoughBalance".to_string()
-                    }
-                    InvalidTxError::RentUnpaid(_, _) => "InvalidTxError::RentUnpaid".to_string(),
-                    InvalidTxError::CostOverflow => "InvalidTxError::CostOverflow".to_string(),
-                    InvalidTxError::InvalidChain => "InvalidTxError::InvalidChain".to_string(),
-                    InvalidTxError::Expired => "InvalidTxError::Expired".to_string(),
-                },
-            },
-        }
-    }
-}
-
-impl From<ActionError> for ExecutionErrorView {
-    fn from(error: ActionError) -> Self {
-        ExecutionError::Action(error).into()
-    }
-}
-
-impl From<InvalidTxError> for ExecutionErrorView {
-    fn from(error: InvalidTxError) -> Self {
-        ExecutionError::InvalidTx(error).into()
-    }
+pub enum ServerError {
+    TxExecutionError(TxExecutionError),
+    Timeout,
+    Closed,
 }
 
 #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Eq, Clone)]
@@ -781,7 +687,7 @@ pub enum ExecutionStatusView {
     /// The execution is pending or unknown.
     Unknown,
     /// The execution has failed.
-    Failure(ExecutionErrorView),
+    Failure(TxExecutionError),
     /// The final action succeeded and returned some value or an empty vec encoded in base64.
     SuccessValue(String),
     /// The final action of the receipt returned a promise or the signed transaction was converted
@@ -809,7 +715,7 @@ impl From<ExecutionStatus> for ExecutionStatusView {
     fn from(outcome: ExecutionStatus) -> Self {
         match outcome {
             ExecutionStatus::Unknown => ExecutionStatusView::Unknown,
-            ExecutionStatus::Failure(e) => ExecutionStatusView::Failure(e.into()),
+            ExecutionStatus::Failure(e) => ExecutionStatusView::Failure(e),
             ExecutionStatus::SuccessValue(v) => ExecutionStatusView::SuccessValue(to_base64(&v)),
             ExecutionStatus::SuccessReceiptId(receipt_id) => {
                 ExecutionStatusView::SuccessReceiptId(receipt_id)
diff --git a/near/src/runtime.rs b/near/src/runtime.rs
index ad52c95e052..b588b05aef2 100644
--- a/near/src/runtime.rs
+++ b/near/src/runtime.rs
@@ -306,7 +306,7 @@ impl NightshadeRuntime {
             )
             .map_err(|e| match e {
                 RuntimeError::InvalidTxError(_) => ErrorKind::InvalidTransactions,
-                RuntimeError::BalanceMismatch(e) => panic!("{}", e),
+                RuntimeError::BalanceMismatchError(e) => panic!("{}", e),
                 // TODO: process gracefully
                 RuntimeError::UnexpectedIntegerOverflow => {
                     panic!("RuntimeError::UnexpectedIntegerOverflow")
diff --git a/runtime/near-vm-errors/Cargo.toml b/runtime/near-vm-errors/Cargo.toml
index 72f6b34ffca..03d0941fa57 100644
--- a/runtime/near-vm-errors/Cargo.toml
+++ b/runtime/near-vm-errors/Cargo.toml
@@ -13,3 +13,9 @@ Error that can occur inside Near Runtime encapsulated in a separate crate. Might
 """
 
 [dependencies]
+borsh = "0.2.10"
+near-rpc-error-macro = { path = "../../tools/rpctypegen/macro" }
+serde = { version = "1.0", features = ["derive"] }
+
+[features]
+dump_errors_schema = ["near-rpc-error-macro/dump_errors_schema"]
diff --git a/runtime/near-vm-errors/src/lib.rs b/runtime/near-vm-errors/src/lib.rs
index 014ec376f11..ec67da2d937 100644
--- a/runtime/near-vm-errors/src/lib.rs
+++ b/runtime/near-vm-errors/src/lib.rs
@@ -1,21 +1,31 @@
+use borsh::{BorshDeserialize, BorshSerialize};
+use near_rpc_error_macro::RpcError;
+use serde::{Deserialize, Serialize};
 use std::fmt;
 
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(
+    Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError,
+)]
 pub enum VMError {
-    FunctionCallError(FunctionCallError),
+    FunctionExecError(FunctionExecError),
+    // TODO: serialize/deserialize?
     StorageError(Vec<u8>),
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum FunctionCallError {
+#[derive(
+    Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError,
+)]
+pub enum FunctionExecError {
     CompilationError(CompilationError),
-    LinkError(String),
-    ResolveError(MethodResolveError),
-    WasmTrap(String),
+    LinkError { msg: String },
+    MethodResolveError(MethodResolveError),
+    WasmTrap { msg: String },
     HostError(HostError),
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(
+    Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError,
+)]
 pub enum MethodResolveError {
     MethodEmptyName,
     MethodUTF8Error,
@@ -23,71 +33,92 @@ pub enum MethodResolveError {
     MethodInvalidSignature,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(
+    Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError,
+)]
 pub enum CompilationError {
-    CodeDoesNotExist(String),
+    CodeDoesNotExist { account_id: String },
     PrepareError(PrepareError),
-    WasmerCompileError(String),
+    WasmerCompileError { msg: String },
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(
+    Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError,
+)]
 /// Error that can occur while preparing or executing Wasm smart-contract.
 pub enum PrepareError {
     /// Error happened while serializing the module.
     Serialization,
-
     /// Error happened while deserializing the module.
     Deserialization,
-
     /// Internal memory declaration has been found in the module.
     InternalMemoryDeclared,
-
     /// Gas instrumentation failed.
     ///
     /// This most likely indicates the module isn't valid.
     GasInstrumentation,
-
     /// Stack instrumentation failed.
     ///
     /// This  most likely indicates the module isn't valid.
     StackHeightInstrumentation,
-
     /// Error happened during instantiation.
     ///
     /// This might indicate that `start` function trapped, or module isn't
     /// instantiable and/or unlinkable.
     Instantiate,
-
     /// Error creating memory.
     Memory,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(
+    Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError,
+)]
 pub enum HostError {
+    /// String encoding is bad UTF-16 sequence
     BadUTF16,
+    /// String encoding is bad UTF-8 sequence
     BadUTF8,
+    /// Exceeded the prepaid gas
     GasExceeded,
+    /// Exceeded the maximum amount of gas allowed to burn per contract
     GasLimitExceeded,
+    /// Exceeded the account balance
     BalanceExceeded,
+    /// Tried to call an empty method name
     EmptyMethodName,
-    GuestPanic(String),
+    /// Smart contract panicked
+    GuestPanic { panic_msg: String },
+    /// IntegerOverflow happened during a contract execution
     IntegerOverflow,
-    InvalidPromiseIndex(u64),
+    /// `promise_idx` does not correspond to existing promises
+    InvalidPromiseIndex { promise_idx: u64 },
+    /// Actions can only be appended to non-joint promise.
     CannotAppendActionToJointPromise,
+    /// Returning joint promise is currently prohibited
     CannotReturnJointPromise,
-    InvalidPromiseResultIndex(u64),
-    InvalidRegisterId(u64),
-    IteratorWasInvalidated(u64),
+    /// Accessed invalid promise result index
+    InvalidPromiseResultIndex { result_idx: u64 },
+    /// Accessed invalid register id
+    InvalidRegisterId { register_id: u64 },
+    /// Iterator `iterator_index` was invalidated after its creation by performing a mutable operation on trie
+    IteratorWasInvalidated { iterator_index: u64 },
+    /// Accessed memory outside the bounds
     MemoryAccessViolation,
-    InvalidReceiptIndex(u64),
-    InvalidIteratorIndex(u64),
+    /// VM Logic returned an invalid receipt index
+    InvalidReceiptIndex { receipt_index: u64 },
+    /// Iterator index `iterator_index` does not exist
+    InvalidIteratorIndex { iterator_index: u64 },
+    /// VM Logic returned an invalid account id
     InvalidAccountId,
+    /// VM Logic returned an invalid method name
     InvalidMethodName,
+    /// VM Logic provided an invalid public key
     InvalidPublicKey,
-    ProhibitedInView(String),
+    /// `method_name` is not allowed in view calls
+    ProhibitedInView { method_name: String },
 }
 
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, BorshDeserialize, BorshSerialize, Deserialize, Serialize)]
 pub enum HostErrorOrStorageError {
     HostError(HostError),
     /// Error from underlying storage, serialized
@@ -102,7 +133,7 @@ impl From<HostError> for HostErrorOrStorageError {
 
 impl From<PrepareError> for VMError {
     fn from(err: PrepareError) -> Self {
-        VMError::FunctionCallError(FunctionCallError::CompilationError(
+        VMError::FunctionExecError(FunctionExecError::CompilationError(
             CompilationError::PrepareError(err),
         ))
     }
@@ -125,14 +156,14 @@ impl fmt::Display for PrepareError {
     }
 }
 
-impl fmt::Display for FunctionCallError {
+impl fmt::Display for FunctionExecError {
     fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
         match self {
-            FunctionCallError::CompilationError(e) => e.fmt(f),
-            FunctionCallError::ResolveError(e) => e.fmt(f),
-            FunctionCallError::HostError(e) => e.fmt(f),
-            FunctionCallError::LinkError(s) => write!(f, "{}", s),
-            FunctionCallError::WasmTrap(s) => write!(f, "WebAssembly trap: {}", s),
+            FunctionExecError::CompilationError(e) => e.fmt(f),
+            FunctionExecError::MethodResolveError(e) => e.fmt(f),
+            FunctionExecError::HostError(e) => e.fmt(f),
+            FunctionExecError::LinkError { msg } => write!(f, "{}", msg),
+            FunctionExecError::WasmTrap { msg } => write!(f, "WebAssembly trap: {}", msg),
         }
     }
 }
@@ -140,11 +171,13 @@ impl fmt::Display for FunctionCallError {
 impl fmt::Display for CompilationError {
     fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
         match self {
-            CompilationError::CodeDoesNotExist(account_id) => {
+            CompilationError::CodeDoesNotExist { account_id } => {
                 write!(f, "cannot find contract code for account {}", account_id)
             }
             CompilationError::PrepareError(p) => write!(f, "PrepareError: {}", p),
-            CompilationError::WasmerCompileError(s) => write!(f, "Wasmer compilation error: {}", s),
+            CompilationError::WasmerCompileError { msg } => {
+                write!(f, "Wasmer compilation error: {}", msg)
+            }
         }
     }
 }
@@ -158,7 +191,7 @@ impl fmt::Display for MethodResolveError {
 impl fmt::Display for VMError {
     fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
         match self {
-            VMError::FunctionCallError(err) => fmt::Display::fmt(err, f),
+            VMError::FunctionExecError(err) => fmt::Display::fmt(err, f),
             VMError::StorageError(_err) => write!(f, "StorageError"),
         }
     }
@@ -174,41 +207,41 @@ impl std::fmt::Display for HostError {
             GasLimitExceeded => write!(f, "Exceeded the maximum amount of gas allowed to burn per contract."),
             BalanceExceeded => write!(f, "Exceeded the account balance."),
             EmptyMethodName => write!(f, "Tried to call an empty method name."),
-            GuestPanic(s) => write!(f, "Smart contract panicked: {}", s),
+            GuestPanic{ panic_msg } => write!(f, "Smart contract panicked: {}", panic_msg),
             IntegerOverflow => write!(f, "Integer overflow."),
-            InvalidIteratorIndex(index) => write!(f, "Iterator index {:?} does not exist", index),
-            InvalidPromiseIndex(index) => write!(f, "{:?} does not correspond to existing promises", index),
+            InvalidIteratorIndex{iterator_index} => write!(f, "Iterator index {:?} does not exist", iterator_index),
+            InvalidPromiseIndex{promise_idx} => write!(f, "{:?} does not correspond to existing promises", promise_idx),
             CannotAppendActionToJointPromise => write!(f, "Actions can only be appended to non-joint promise."),
             CannotReturnJointPromise => write!(f, "Returning joint promise is currently prohibited."),
-            InvalidPromiseResultIndex(index) => write!(f, "Accessed invalid promise result index: {:?}", index),
-            InvalidRegisterId(id) => write!(f, "Accessed invalid register id: {:?}", id),
-            IteratorWasInvalidated(index) => write!(f, "Iterator {:?} was invalidated after its creation by performing a mutable operation on trie", index),
+            InvalidPromiseResultIndex{result_idx} => write!(f, "Accessed invalid promise result index: {:?}", result_idx),
+            InvalidRegisterId{register_id} => write!(f, "Accessed invalid register id: {:?}", register_id),
+            IteratorWasInvalidated{iterator_index} => write!(f, "Iterator {:?} was invalidated after its creation by performing a mutable operation on trie", iterator_index),
             MemoryAccessViolation => write!(f, "Accessed memory outside the bounds."),
-            InvalidReceiptIndex(index) => write!(f, "VM Logic returned an invalid receipt index: {:?}", index),
+            InvalidReceiptIndex{receipt_index} => write!(f, "VM Logic returned an invalid receipt index: {:?}", receipt_index),
             InvalidAccountId => write!(f, "VM Logic returned an invalid account id"),
             InvalidMethodName => write!(f, "VM Logic returned an invalid method name"),
             InvalidPublicKey => write!(f, "VM Logic provided an invalid public key"),
-            ProhibitedInView(method_name) => write!(f, "{} is not allowed in view calls", method_name),
+            ProhibitedInView{method_name} => write!(f, "{} is not allowed in view calls", method_name),
         }
     }
 }
 
 #[cfg(test)]
 mod tests {
-    use crate::{CompilationError, FunctionCallError, MethodResolveError, PrepareError, VMError};
+    use crate::{CompilationError, FunctionExecError, MethodResolveError, PrepareError, VMError};
 
     #[test]
     fn test_display() {
         // TODO: proper printing
         assert_eq!(
-            VMError::FunctionCallError(FunctionCallError::ResolveError(
+            VMError::FunctionExecError(FunctionExecError::MethodResolveError(
                 MethodResolveError::MethodInvalidSignature
             ))
             .to_string(),
             "MethodInvalidSignature"
         );
         assert_eq!(
-            VMError::FunctionCallError(FunctionCallError::CompilationError(
+            VMError::FunctionExecError(FunctionExecError::CompilationError(
                 CompilationError::PrepareError(PrepareError::StackHeightInstrumentation)
             ))
             .to_string(),
diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs
index 6d4197ae030..a85cdc8818c 100644
--- a/runtime/near-vm-logic/src/logic.rs
+++ b/runtime/near-vm-logic/src/logic.rs
@@ -203,7 +203,7 @@ impl<'a> VMLogic<'a> {
             self.gas_counter.pay_per_byte(read_register_byte, data.len() as _)?;
             Ok(data.clone())
         } else {
-            Err(HostError::InvalidRegisterId(register_id).into())
+            Err(HostError::InvalidRegisterId { register_id }.into())
         }
     }
 
@@ -424,7 +424,10 @@ impl<'a> VMLogic<'a> {
         self.gas_counter.pay_base(base)?;
 
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView("signer_account_id".to_string()).into());
+            return Err(HostError::ProhibitedInView {
+                method_name: "signer_account_id".to_string(),
+            }
+            .into());
         }
         self.internal_write_register(
             register_id,
@@ -448,7 +451,10 @@ impl<'a> VMLogic<'a> {
         self.gas_counter.pay_base(base)?;
 
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView("signer_account_pk".to_string()).into());
+            return Err(HostError::ProhibitedInView {
+                method_name: "signer_account_pk".to_string(),
+            }
+            .into());
         }
         self.internal_write_register(register_id, self.context.signer_account_pk.clone())
     }
@@ -469,7 +475,10 @@ impl<'a> VMLogic<'a> {
         self.gas_counter.pay_base(base)?;
 
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView("predecessor_account_id".to_string()).into());
+            return Err(HostError::ProhibitedInView {
+                method_name: "predecessor_account_id".to_string(),
+            }
+            .into());
         }
         self.internal_write_register(
             register_id,
@@ -565,7 +574,10 @@ impl<'a> VMLogic<'a> {
         self.gas_counter.pay_base(base)?;
 
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView("attached_deposit".to_string()).into());
+            return Err(HostError::ProhibitedInView {
+                method_name: "attached_deposit".to_string(),
+            }
+            .into());
         }
         self.memory_set_u128(balance_ptr, self.context.attached_deposit)
     }
@@ -582,7 +594,9 @@ impl<'a> VMLogic<'a> {
     pub fn prepaid_gas(&mut self) -> Result<Gas> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView("prepaid_gas".to_string()).into());
+            return Err(
+                HostError::ProhibitedInView { method_name: "prepaid_gas".to_string() }.into()
+            );
         }
         Ok(self.context.prepaid_gas)
     }
@@ -599,7 +613,7 @@ impl<'a> VMLogic<'a> {
     pub fn used_gas(&mut self) -> Result<Gas> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView("used_gas".to_string()).into());
+            return Err(HostError::ProhibitedInView { method_name: "used_gas".to_string() }.into());
         }
         Ok(self.gas_counter.used_gas())
     }
@@ -811,7 +825,9 @@ impl<'a> VMLogic<'a> {
     ) -> Result<PromiseIndex> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView("promise_and".to_string()).into());
+            return Err(
+                HostError::ProhibitedInView { method_name: "promise_and".to_string() }.into()
+            );
         }
         self.gas_counter.pay_base(promise_and_base)?;
         self.gas_counter.pay_per_byte(
@@ -828,7 +844,7 @@ impl<'a> VMLogic<'a> {
             let promise = self
                 .promises
                 .get(*promise_idx as usize)
-                .ok_or(HostError::InvalidPromiseIndex(*promise_idx))?;
+                .ok_or(HostError::InvalidPromiseIndex { promise_idx: *promise_idx })?;
             match &promise {
                 Promise::Receipt(receipt_idx) => {
                     receipt_dependencies.push(*receipt_idx);
@@ -867,7 +883,10 @@ impl<'a> VMLogic<'a> {
     ) -> Result<u64> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView("promise_batch_create".to_string()).into());
+            return Err(HostError::ProhibitedInView {
+                method_name: "promise_batch_create".to_string(),
+            }
+            .into());
         }
         let account_id = self.read_and_parse_account_id(account_id_ptr, account_id_len)?;
         let sir = account_id == self.context.current_account_id;
@@ -907,14 +926,17 @@ impl<'a> VMLogic<'a> {
     ) -> Result<u64> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView("promise_batch_then".to_string()).into());
+            return Err(HostError::ProhibitedInView {
+                method_name: "promise_batch_then".to_string(),
+            }
+            .into());
         }
         let account_id = self.read_and_parse_account_id(account_id_ptr, account_id_len)?;
         // Update the DAG and return new promise idx.
         let promise = self
             .promises
             .get(promise_idx as usize)
-            .ok_or(HostError::InvalidPromiseIndex(promise_idx))?;
+            .ok_or(HostError::InvalidPromiseIndex { promise_idx })?;
         let receipt_dependencies = match &promise {
             Promise::Receipt(receipt_idx) => vec![*receipt_idx],
             Promise::NotReceipt(receipt_indices) => receipt_indices.clone(),
@@ -949,7 +971,7 @@ impl<'a> VMLogic<'a> {
         let promise = self
             .promises
             .get(promise_idx as usize)
-            .ok_or(HostError::InvalidPromiseIndex(promise_idx))?;
+            .ok_or(HostError::InvalidPromiseIndex { promise_idx })?;
         let receipt_idx = match &promise {
             Promise::Receipt(receipt_idx) => Ok(*receipt_idx),
             Promise::NotReceipt(_) => Err(HostError::CannotAppendActionToJointPromise),
@@ -980,9 +1002,9 @@ impl<'a> VMLogic<'a> {
     pub fn promise_batch_action_create_account(&mut self, promise_idx: u64) -> Result<()> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView(
-                "promise_batch_action_create_account".to_string(),
-            )
+            return Err(HostError::ProhibitedInView {
+                method_name: "promise_batch_action_create_account".to_string(),
+            }
             .into());
         }
         let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?;
@@ -1018,9 +1040,9 @@ impl<'a> VMLogic<'a> {
     ) -> Result<()> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView(
-                "promise_batch_action_deploy_contract".to_string(),
-            )
+            return Err(HostError::ProhibitedInView {
+                method_name: "promise_batch_action_deploy_contract".to_string(),
+            }
             .into());
         }
         let code = self.get_vec_from_memory_or_register(code_ptr, code_len)?;
@@ -1070,9 +1092,9 @@ impl<'a> VMLogic<'a> {
     ) -> Result<()> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView(
-                "promise_batch_action_function_call".to_string(),
-            )
+            return Err(HostError::ProhibitedInView {
+                method_name: "promise_batch_action_function_call".to_string(),
+            }
             .into());
         }
         let amount = self.memory_get_u128(amount_ptr)?;
@@ -1124,9 +1146,10 @@ impl<'a> VMLogic<'a> {
     ) -> Result<()> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(
-                HostError::ProhibitedInView("promise_batch_action_transfer".to_string()).into()
-            );
+            return Err(HostError::ProhibitedInView {
+                method_name: "promise_batch_action_transfer".to_string(),
+            }
+            .into());
         }
         let amount = self.memory_get_u128(amount_ptr)?;
 
@@ -1167,9 +1190,10 @@ impl<'a> VMLogic<'a> {
     ) -> Result<()> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(
-                HostError::ProhibitedInView("promise_batch_action_stake".to_string()).into()
-            );
+            return Err(HostError::ProhibitedInView {
+                method_name: "promise_batch_action_stake".to_string(),
+            }
+            .into());
         }
         let amount = self.memory_get_u128(amount_ptr)?;
         let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?;
@@ -1211,9 +1235,9 @@ impl<'a> VMLogic<'a> {
     ) -> Result<()> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView(
-                "promise_batch_action_add_key_with_full_access".to_string(),
-            )
+            return Err(HostError::ProhibitedInView {
+                method_name: "promise_batch_action_add_key_with_full_access".to_string(),
+            }
             .into());
         }
         let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?;
@@ -1262,9 +1286,9 @@ impl<'a> VMLogic<'a> {
     ) -> Result<()> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView(
-                "promise_batch_action_add_key_with_function_call".to_string(),
-            )
+            return Err(HostError::ProhibitedInView {
+                method_name: "promise_batch_action_add_key_with_function_call".to_string(),
+            }
             .into());
         }
         let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?;
@@ -1336,9 +1360,10 @@ impl<'a> VMLogic<'a> {
     ) -> Result<()> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(
-                HostError::ProhibitedInView("promise_batch_action_delete_key".to_string()).into()
-            );
+            return Err(HostError::ProhibitedInView {
+                method_name: "promise_batch_action_delete_key".to_string(),
+            }
+            .into());
         }
         let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?;
 
@@ -1375,9 +1400,9 @@ impl<'a> VMLogic<'a> {
     ) -> Result<()> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView(
-                "promise_batch_action_delete_account".to_string(),
-            )
+            return Err(HostError::ProhibitedInView {
+                method_name: "promise_batch_action_delete_account".to_string(),
+            }
             .into());
         }
         let beneficiary_id =
@@ -1409,7 +1434,10 @@ impl<'a> VMLogic<'a> {
     pub fn promise_results_count(&mut self) -> Result<u64> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView("promise_results_count".to_string()).into());
+            return Err(HostError::ProhibitedInView {
+                method_name: "promise_results_count".to_string(),
+            }
+            .into());
         }
         Ok(self.promise_results.len() as _)
     }
@@ -1439,12 +1467,14 @@ impl<'a> VMLogic<'a> {
     pub fn promise_result(&mut self, result_idx: u64, register_id: u64) -> Result<u64> {
         self.gas_counter.pay_base(base)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView("promise_result".to_string()).into());
+            return Err(
+                HostError::ProhibitedInView { method_name: "promise_result".to_string() }.into()
+            );
         }
         match self
             .promise_results
             .get(result_idx as usize)
-            .ok_or(HostError::InvalidPromiseResultIndex(result_idx))?
+            .ok_or(HostError::InvalidPromiseResultIndex { result_idx })?
         {
             PromiseResult::NotReady => Ok(0),
             PromiseResult::Successful(data) => {
@@ -1470,12 +1500,14 @@ impl<'a> VMLogic<'a> {
         self.gas_counter.pay_base(base)?;
         self.gas_counter.pay_base(promise_return)?;
         if self.context.is_view {
-            return Err(HostError::ProhibitedInView("promise_return".to_string()).into());
+            return Err(
+                HostError::ProhibitedInView { method_name: "promise_return".to_string() }.into()
+            );
         }
         match self
             .promises
             .get(promise_idx as usize)
-            .ok_or(HostError::InvalidPromiseIndex(promise_idx))?
+            .ok_or(HostError::InvalidPromiseIndex { promise_idx })?
         {
             Promise::Receipt(receipt_idx) => {
                 self.return_data = ReturnData::ReceiptIndex(*receipt_idx);
@@ -1537,7 +1569,7 @@ impl<'a> VMLogic<'a> {
     /// `base`
     pub fn panic(&mut self) -> Result<()> {
         self.gas_counter.pay_base(base)?;
-        Err(HostError::GuestPanic("explicit guest panic".to_string()).into())
+        Err(HostError::GuestPanic { panic_msg: "explicit guest panic".to_string() }.into())
     }
 
     /// Guest panics with the UTF-8 encoded string.
@@ -1553,7 +1585,7 @@ impl<'a> VMLogic<'a> {
     /// `base + cost of reading and decoding a utf8 string`
     pub fn panic_utf8(&mut self, len: u64, ptr: u64) -> Result<()> {
         self.gas_counter.pay_base(base)?;
-        Err(HostError::GuestPanic(self.get_utf8_string(len, ptr)?).into())
+        Err(HostError::GuestPanic { panic_msg: self.get_utf8_string(len, ptr)? }.into())
     }
 
     /// Logs the UTF-8 encoded string.
@@ -1623,7 +1655,7 @@ impl<'a> VMLogic<'a> {
         self.gas_counter.pay_per_byte(log_byte, message.as_bytes().len() as u64)?;
         self.logs.push(format!("ABORT: {}", message));
 
-        Err(HostError::GuestPanic(message).into())
+        Err(HostError::GuestPanic { panic_msg: message }.into())
     }
 
     // ###############
@@ -1937,9 +1969,9 @@ impl<'a> VMLogic<'a> {
         self.gas_counter.pay_base(base)?;
         self.gas_counter.pay_base(storage_iter_next_base)?;
         if self.invalid_iterators.contains(&iterator_id) {
-            return Err(HostError::IteratorWasInvalidated(iterator_id).into());
+            return Err(HostError::IteratorWasInvalidated { iterator_index: iterator_id }.into());
         } else if !self.valid_iterators.contains(&iterator_id) {
-            return Err(HostError::InvalidIteratorIndex(iterator_id).into());
+            return Err(HostError::InvalidIteratorIndex { iterator_index: iterator_id }.into());
         }
 
         let nodes_before = self.ext.get_touched_nodes_count();
diff --git a/runtime/near-vm-logic/src/mocks/mock_external.rs b/runtime/near-vm-logic/src/mocks/mock_external.rs
index 60ccbc9c367..6df3a098c21 100644
--- a/runtime/near-vm-logic/src/mocks/mock_external.rs
+++ b/runtime/near-vm-logic/src/mocks/mock_external.rs
@@ -123,13 +123,13 @@ impl External for MockedExternal {
                 }
                 None => Ok(None),
             },
-            None => Err(HostError::InvalidIteratorIndex(iterator_idx).into()),
+            None => Err(HostError::InvalidIteratorIndex { iterator_index: iterator_idx }.into()),
         }
     }
 
     fn storage_iter_drop(&mut self, iterator_idx: u64) -> Result<()> {
         if self.iterators.remove(&iterator_idx).is_none() {
-            Err(HostError::InvalidIteratorIndex(iterator_idx).into())
+            Err(HostError::InvalidIteratorIndex { iterator_index: iterator_idx }.into())
         } else {
             Ok(())
         }
@@ -137,7 +137,7 @@ impl External for MockedExternal {
 
     fn create_receipt(&mut self, receipt_indices: Vec<u64>, receiver_id: String) -> Result<u64> {
         if let Some(index) = receipt_indices.iter().find(|&&el| el >= self.receipts.len() as u64) {
-            return Err(HostError::InvalidReceiptIndex(*index).into());
+            return Err(HostError::InvalidReceiptIndex { receipt_index: *index }.into());
         }
         let res = self.receipts.len() as u64;
         self.receipts.push(Receipt { receipt_indices, receiver_id, actions: vec![] });
diff --git a/runtime/near-vm-logic/tests/test_registers.rs b/runtime/near-vm-logic/tests/test_registers.rs
index ddc2cc132cd..7e69cf63491 100644
--- a/runtime/near-vm-logic/tests/test_registers.rs
+++ b/runtime/near-vm-logic/tests/test_registers.rs
@@ -26,7 +26,7 @@ fn test_non_existent_register() {
     let buffer = [0u8; 3];
     assert_eq!(
         logic.read_register(0, buffer.as_ptr() as u64),
-        Err(HostError::InvalidRegisterId(0).into())
+        Err(HostError::InvalidRegisterId { register_id: 0 }.into())
     );
 }
 
diff --git a/runtime/near-vm-runner/src/errors.rs b/runtime/near-vm-runner/src/errors.rs
index 34b604c76af..76d96eaacc6 100644
--- a/runtime/near-vm-runner/src/errors.rs
+++ b/runtime/near-vm-runner/src/errors.rs
@@ -1,4 +1,4 @@
-use near_vm_errors::{CompilationError, FunctionCallError, MethodResolveError, VMError};
+use near_vm_errors::{CompilationError, FunctionExecError, MethodResolveError, VMError};
 use near_vm_logic::HostErrorOrStorageError;
 
 pub trait IntoVMError {
@@ -10,9 +10,9 @@ impl IntoVMError for wasmer_runtime::error::Error {
         use wasmer_runtime::error::Error;
         match self {
             Error::CompileError(err) => err.into_vm_error(),
-            Error::LinkError(err) => VMError::FunctionCallError(FunctionCallError::LinkError(
-                format!("{:.500}", Error::LinkError(err).to_string()),
-            )),
+            Error::LinkError(err) => VMError::FunctionExecError(FunctionExecError::LinkError {
+                msg: format!("{:.500}", Error::LinkError(err).to_string()),
+            }),
             Error::RuntimeError(err) => err.into_vm_error(),
             Error::ResolveError(err) => err.into_vm_error(),
             Error::CallError(err) => err.into_vm_error(),
@@ -33,8 +33,8 @@ impl IntoVMError for wasmer_runtime::error::CallError {
 
 impl IntoVMError for wasmer_runtime::error::CompileError {
     fn into_vm_error(self) -> VMError {
-        VMError::FunctionCallError(FunctionCallError::CompilationError(
-            CompilationError::WasmerCompileError(self.to_string()),
+        VMError::FunctionExecError(FunctionExecError::CompilationError(
+            CompilationError::WasmerCompileError { msg: self.to_string() },
         ))
     }
 }
@@ -43,14 +43,14 @@ impl IntoVMError for wasmer_runtime::error::ResolveError {
     fn into_vm_error(self) -> VMError {
         use wasmer_runtime::error::ResolveError as WasmerResolveError;
         match self {
-            WasmerResolveError::Signature { .. } => VMError::FunctionCallError(
-                FunctionCallError::ResolveError(MethodResolveError::MethodInvalidSignature),
+            WasmerResolveError::Signature { .. } => VMError::FunctionExecError(
+                FunctionExecError::MethodResolveError(MethodResolveError::MethodInvalidSignature),
             ),
-            WasmerResolveError::ExportNotFound { .. } => VMError::FunctionCallError(
-                FunctionCallError::ResolveError(MethodResolveError::MethodNotFound),
+            WasmerResolveError::ExportNotFound { .. } => VMError::FunctionExecError(
+                FunctionExecError::MethodResolveError(MethodResolveError::MethodNotFound),
             ),
-            WasmerResolveError::ExportWrongType { .. } => VMError::FunctionCallError(
-                FunctionCallError::ResolveError(MethodResolveError::MethodNotFound),
+            WasmerResolveError::ExportWrongType { .. } => VMError::FunctionExecError(
+                FunctionExecError::MethodResolveError(MethodResolveError::MethodNotFound),
             ),
         }
     }
@@ -61,7 +61,7 @@ impl IntoVMError for wasmer_runtime::error::RuntimeError {
         use wasmer_runtime::error::RuntimeError;
         match &self {
             RuntimeError::Trap { msg } => {
-                VMError::FunctionCallError(FunctionCallError::WasmTrap(msg.to_string()))
+                VMError::FunctionExecError(FunctionExecError::WasmTrap { msg: msg.to_string() })
             }
             RuntimeError::Error { data } => {
                 if let Some(err) = data.downcast_ref::<HostErrorOrStorageError>() {
@@ -70,7 +70,7 @@ impl IntoVMError for wasmer_runtime::error::RuntimeError {
                             VMError::StorageError(s.clone())
                         }
                         HostErrorOrStorageError::HostError(h) => {
-                            VMError::FunctionCallError(FunctionCallError::HostError(h.clone()))
+                            VMError::FunctionExecError(FunctionExecError::HostError(h.clone()))
                         }
                     }
                 } else {
@@ -79,7 +79,9 @@ impl IntoVMError for wasmer_runtime::error::RuntimeError {
                         data.type_id(),
                         self.to_string()
                     );
-                    VMError::FunctionCallError(FunctionCallError::WasmTrap("unknown".to_string()))
+                    VMError::FunctionExecError(FunctionExecError::WasmTrap {
+                        msg: "unknown".to_string(),
+                    })
                 }
             }
         }
diff --git a/runtime/near-vm-runner/src/runner.rs b/runtime/near-vm-runner/src/runner.rs
index 360a1001c39..be724759793 100644
--- a/runtime/near-vm-runner/src/runner.rs
+++ b/runtime/near-vm-runner/src/runner.rs
@@ -2,7 +2,7 @@ use crate::errors::IntoVMError;
 use crate::memory::WasmerMemory;
 use crate::{cache, imports};
 use near_runtime_fees::RuntimeFeesConfig;
-use near_vm_errors::{FunctionCallError, MethodResolveError, VMError};
+use near_vm_errors::{FunctionExecError, MethodResolveError, VMError};
 use near_vm_logic::types::PromiseResult;
 use near_vm_logic::{External, VMConfig, VMContext, VMLogic, VMOutcome};
 use wasmer_runtime::Module;
@@ -16,12 +16,12 @@ fn check_method(module: &Module, method_name: &str) -> Result<(), VMError> {
         if sig.params().is_empty() && sig.returns().is_empty() {
             Ok(())
         } else {
-            Err(VMError::FunctionCallError(FunctionCallError::ResolveError(
+            Err(VMError::FunctionExecError(FunctionExecError::MethodResolveError(
                 MethodResolveError::MethodInvalidSignature,
             )))
         }
     } else {
-        Err(VMError::FunctionCallError(FunctionCallError::ResolveError(
+        Err(VMError::FunctionExecError(FunctionExecError::MethodResolveError(
             MethodResolveError::MethodNotFound,
         )))
     }
@@ -57,7 +57,7 @@ pub fn run<'a>(
     if method_name.is_empty() {
         return (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::ResolveError(
+            Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError(
                 MethodResolveError::MethodEmptyName,
             ))),
         );
@@ -83,7 +83,7 @@ pub fn run<'a>(
         Err(_) => {
             return (
                 None,
-                Some(VMError::FunctionCallError(FunctionCallError::ResolveError(
+                Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError(
                     MethodResolveError::MethodUTF8Error,
                 ))),
             )
diff --git a/runtime/near-vm-runner/tests/test_error_cases.rs b/runtime/near-vm-runner/tests/test_error_cases.rs
index c8d5577b719..cff219058b0 100644
--- a/runtime/near-vm-runner/tests/test_error_cases.rs
+++ b/runtime/near-vm-runner/tests/test_error_cases.rs
@@ -1,5 +1,5 @@
 use crate::utils::{make_simple_contract_call, make_simple_contract_call_with_gas};
-use near_vm_errors::{CompilationError, FunctionCallError, MethodResolveError, PrepareError};
+use near_vm_errors::{CompilationError, FunctionExecError, MethodResolveError, PrepareError};
 use near_vm_logic::{HostError, ReturnData, VMOutcome};
 use near_vm_runner::VMError;
 
@@ -36,7 +36,7 @@ fn test_infinite_initializer() {
         make_simple_contract_call(&infinite_initializer_contract(), b"hello"),
         (
             Some(vm_outcome_with_gas(100000000000000)),
-            Some(VMError::FunctionCallError(FunctionCallError::HostError(HostError::GasExceeded)))
+            Some(VMError::FunctionExecError(FunctionExecError::HostError(HostError::GasExceeded)))
         )
     );
 }
@@ -47,7 +47,7 @@ fn test_infinite_initializer_export_not_found() {
         make_simple_contract_call(&infinite_initializer_contract(), b"hello2"),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::ResolveError(
+            Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError(
                 MethodResolveError::MethodNotFound
             )))
         )
@@ -80,7 +80,7 @@ fn test_export_not_found() {
         make_simple_contract_call(&simple_contract(), b"hello2"),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::ResolveError(
+            Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError(
                 MethodResolveError::MethodNotFound
             )))
         )
@@ -93,7 +93,7 @@ fn test_empty_method() {
         make_simple_contract_call(&simple_contract(), b""),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::ResolveError(
+            Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError(
                 MethodResolveError::MethodEmptyName
             )))
         )
@@ -106,7 +106,7 @@ fn test_invalid_utf8() {
         make_simple_contract_call(&simple_contract(), &[255u8]),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::ResolveError(
+            Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError(
                 MethodResolveError::MethodUTF8Error
             )))
         )
@@ -131,7 +131,9 @@ fn test_trap_contract() {
         make_simple_contract_call(&trap_contract(), b"hello"),
         (
             Some(vm_outcome_with_gas(3856371)),
-            Some(VMError::FunctionCallError(FunctionCallError::WasmTrap("unknown".to_string())))
+            Some(VMError::FunctionExecError(FunctionExecError::WasmTrap {
+                msg: "unknown".to_string()
+            }))
         )
     );
 }
@@ -155,7 +157,9 @@ fn test_trap_initializer() {
         make_simple_contract_call(&trap_initializer(), b"hello"),
         (
             Some(vm_outcome_with_gas(3856371)),
-            Some(VMError::FunctionCallError(FunctionCallError::WasmTrap("unknown".to_string())))
+            Some(VMError::FunctionExecError(FunctionExecError::WasmTrap {
+                msg: "unknown".to_string()
+            }))
         )
     );
 }
@@ -178,7 +182,7 @@ fn test_wrong_signature_contract() {
         make_simple_contract_call(&wrong_signature_contract(), b"hello"),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::ResolveError(
+            Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError(
                 MethodResolveError::MethodInvalidSignature
             )))
         )
@@ -202,7 +206,7 @@ fn test_export_wrong_type() {
         make_simple_contract_call(&export_wrong_type(), b"hello"),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::ResolveError(
+            Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError(
                 MethodResolveError::MethodNotFound
             )))
         )
@@ -228,9 +232,9 @@ fn test_guest_panic() {
         make_simple_contract_call(&guest_panic(), b"hello"),
         (
             Some(vm_outcome_with_gas(130080593)),
-            Some(VMError::FunctionCallError(FunctionCallError::HostError(HostError::GuestPanic(
-                "explicit guest panic".to_string()
-            ))))
+            Some(VMError::FunctionExecError(FunctionExecError::HostError(HostError::GuestPanic {
+                panic_msg: "explicit guest panic".to_string()
+            })))
         )
     );
 }
@@ -253,7 +257,9 @@ fn test_stack_overflow() {
         make_simple_contract_call(&stack_overflow(), b"hello"),
         (
             Some(vm_outcome_with_gas(63182782464)),
-            Some(VMError::FunctionCallError(FunctionCallError::WasmTrap("unknown".to_string())))
+            Some(VMError::FunctionExecError(FunctionExecError::WasmTrap {
+                msg: "unknown".to_string()
+            }))
         )
     );
 }
@@ -283,7 +289,7 @@ fn test_memory_grow() {
         make_simple_contract_call(&memory_grow(), b"hello"),
         (
             Some(vm_outcome_with_gas(100000000000000)),
-            Some(VMError::FunctionCallError(FunctionCallError::HostError(HostError::GasExceeded)))
+            Some(VMError::FunctionExecError(FunctionExecError::HostError(HostError::GasExceeded)))
         )
     );
 }
@@ -325,7 +331,7 @@ fn test_bad_import_1() {
         make_simple_contract_call(&bad_import_global("wtf"), b"hello"),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::CompilationError(
+            Some(VMError::FunctionExecError(FunctionExecError::CompilationError(
                 CompilationError::PrepareError(PrepareError::Instantiate)
             )))
         )
@@ -338,7 +344,7 @@ fn test_bad_import_2() {
         make_simple_contract_call(&bad_import_func("wtf"), b"hello"),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::CompilationError(
+            Some(VMError::FunctionExecError(FunctionExecError::CompilationError(
                 CompilationError::PrepareError(PrepareError::Instantiate)
             )))
         )
@@ -351,9 +357,9 @@ fn test_bad_import_3() {
         make_simple_contract_call(&bad_import_global("env"), b"hello"),
         (
             Some(vm_outcome_with_gas(0)),
-            Some(VMError::FunctionCallError(FunctionCallError::LinkError(
-                "link error: Incorrect import type, namespace: env, name: input, expected type: global, found type: function".to_string()
-            )))
+            Some(VMError::FunctionExecError(FunctionExecError::LinkError{
+                msg: "link error: Incorrect import type, namespace: env, name: input, expected type: global, found type: function".to_string()
+            }))
         )
     );
 }
@@ -364,9 +370,9 @@ fn test_bad_import_4() {
         make_simple_contract_call(&bad_import_func("env"), b"hello"),
         (
             Some(vm_outcome_with_gas(0)),
-            Some(VMError::FunctionCallError(FunctionCallError::LinkError(
-                "link error: Import not found, namespace: env, name: wtf".to_string()
-            )))
+            Some(VMError::FunctionExecError(FunctionExecError::LinkError {
+                msg: "link error: Import not found, namespace: env, name: wtf".to_string()
+            }))
         )
     );
 }
@@ -390,7 +396,7 @@ fn test_initializer_no_gas() {
         make_simple_contract_call_with_gas(&some_initializer_contract(), b"hello", 0),
         (
             Some(vm_outcome_with_gas(0)),
-            Some(VMError::FunctionCallError(FunctionCallError::HostError(HostError::GasExceeded)))
+            Some(VMError::FunctionExecError(FunctionExecError::HostError(HostError::GasExceeded)))
         )
     );
 }
@@ -421,7 +427,7 @@ fn bad_many_imports() -> Vec<u8> {
 fn test_bad_many_imports() {
     let result = make_simple_contract_call(&bad_many_imports(), b"hello");
     assert_eq!(result.0, Some(vm_outcome_with_gas(0)));
-    if let Some(VMError::FunctionCallError(FunctionCallError::LinkError(msg))) = result.1 {
+    if let Some(VMError::FunctionExecError(FunctionExecError::LinkError { msg })) = result.1 {
         eprintln!("{}", msg);
         assert!(msg.len() < 1000, format!("Huge error message: {}", msg.len()));
     } else {
diff --git a/runtime/near-vm-runner/tests/test_invalid_contracts.rs b/runtime/near-vm-runner/tests/test_invalid_contracts.rs
index acfddb042f7..b20f3bc1f46 100644
--- a/runtime/near-vm-runner/tests/test_invalid_contracts.rs
+++ b/runtime/near-vm-runner/tests/test_invalid_contracts.rs
@@ -1,5 +1,5 @@
 use crate::utils::{make_simple_contract_call, wat2wasm_no_validate};
-use near_vm_errors::{CompilationError, FunctionCallError, PrepareError};
+use near_vm_errors::{CompilationError, FunctionExecError, PrepareError};
 use near_vm_runner::VMError;
 
 mod utils;
@@ -22,7 +22,7 @@ fn test_initializer_wrong_signature_contract() {
         make_simple_contract_call(&initializer_wrong_signature_contract(), b"hello"),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::CompilationError(
+            Some(VMError::FunctionExecError(FunctionExecError::CompilationError(
                 CompilationError::PrepareError(PrepareError::Deserialization)
             )))
         )
@@ -45,7 +45,7 @@ fn test_function_not_defined_contract() {
         make_simple_contract_call(&function_not_defined_contract(), b"hello"),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::CompilationError(
+            Some(VMError::FunctionExecError(FunctionExecError::CompilationError(
                 CompilationError::PrepareError(PrepareError::Deserialization)
             )))
         )
@@ -69,7 +69,7 @@ fn test_function_type_not_defined_contract_1() {
         make_simple_contract_call(&function_type_not_defined_contract(1), b"hello"),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::CompilationError(
+            Some(VMError::FunctionExecError(FunctionExecError::CompilationError(
                 CompilationError::PrepareError(PrepareError::Deserialization)
             )))
         )
@@ -83,7 +83,7 @@ fn test_function_type_not_defined_contract_2() {
         make_simple_contract_call(&function_type_not_defined_contract(0), b"hello"),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::CompilationError(
+            Some(VMError::FunctionExecError(FunctionExecError::CompilationError(
                 CompilationError::PrepareError(PrepareError::Deserialization)
             )))
         )
@@ -96,7 +96,7 @@ fn test_garbage_contract() {
         make_simple_contract_call(&[], b"hello"),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::CompilationError(
+            Some(VMError::FunctionExecError(FunctionExecError::CompilationError(
                 CompilationError::PrepareError(PrepareError::Deserialization)
             )))
         )
@@ -121,7 +121,7 @@ fn test_evil_function_index() {
         make_simple_contract_call(&evil_function_index(), b"abort_with_zero"),
         (
             None,
-            Some(VMError::FunctionCallError(FunctionCallError::CompilationError(
+            Some(VMError::FunctionExecError(FunctionExecError::CompilationError(
                 CompilationError::PrepareError(PrepareError::Deserialization)
             )))
         )
diff --git a/runtime/near-vm-runner/tests/test_rs_contract.rs b/runtime/near-vm-runner/tests/test_rs_contract.rs
index a46e0756cfb..b3bdf75390f 100644
--- a/runtime/near-vm-runner/tests/test_rs_contract.rs
+++ b/runtime/near-vm-runner/tests/test_rs_contract.rs
@@ -1,5 +1,5 @@
 use near_runtime_fees::RuntimeFeesConfig;
-use near_vm_errors::FunctionCallError;
+use near_vm_errors::FunctionExecError;
 use near_vm_logic::mocks::mock_external::MockedExternal;
 use near_vm_logic::types::ReturnData;
 use near_vm_logic::{VMConfig, VMContext, VMOutcome};
@@ -174,6 +174,8 @@ pub fn test_out_of_memory() {
     );
     assert_eq!(
         result.1,
-        Some(VMError::FunctionCallError(FunctionCallError::WasmTrap("unknown".to_string())))
+        Some(VMError::FunctionExecError(FunctionExecError::WasmTrap {
+            msg: "unknown".to_string()
+        }))
     );
 }
diff --git a/runtime/near-vm-runner/tests/test_ts_contract.rs b/runtime/near-vm-runner/tests/test_ts_contract.rs
index c3b2c576c3b..88c93fe4581 100644
--- a/runtime/near-vm-runner/tests/test_ts_contract.rs
+++ b/runtime/near-vm-runner/tests/test_ts_contract.rs
@@ -1,5 +1,5 @@
 use near_runtime_fees::RuntimeFeesConfig;
-use near_vm_errors::FunctionCallError;
+use near_vm_errors::FunctionExecError;
 use near_vm_logic::mocks::mock_external::MockedExternal;
 use near_vm_logic::types::ReturnData;
 use near_vm_logic::{External, HostError, VMConfig, VMContext};
@@ -37,9 +37,9 @@ pub fn test_ts_contract() {
     );
     assert_eq!(
         result.1,
-        Some(VMError::FunctionCallError(FunctionCallError::HostError(HostError::GuestPanic(
-            "explicit guest panic".to_string()
-        ))))
+        Some(VMError::FunctionExecError(FunctionExecError::HostError(HostError::GuestPanic {
+            panic_msg: "explicit guest panic".to_string()
+        })))
     );
 
     // Call method that writes something into storage.
diff --git a/runtime/runtime/Cargo.toml b/runtime/runtime/Cargo.toml
index fe582aff2a2..6ba44a6d218 100644
--- a/runtime/runtime/Cargo.toml
+++ b/runtime/runtime/Cargo.toml
@@ -30,6 +30,7 @@ near-vm-errors = { path = "../../runtime/near-vm-errors" }
 
 [features]
 default = []
+dump_errors_schema = ["near-vm-errors/dump_errors_schema"]
 
 # Use this feature to enable counting of fees and costs applied.
 costs_counting = ["near-vm-logic/costs_counting", "near-vm-runner/costs_counting"]
diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs
index 6be9fba1ab0..d83517238da 100644
--- a/runtime/runtime/src/actions.rs
+++ b/runtime/runtime/src/actions.rs
@@ -26,8 +26,8 @@ use near_vm_logic::VMContext;
 use crate::config::RuntimeConfig;
 use crate::ext::RuntimeExt;
 use crate::{ActionResult, ApplyState};
-use near_primitives::errors::ActionError;
-use near_vm_errors::{CompilationError, FunctionCallError};
+use near_primitives::errors::{ActionError, ActionErrorKind};
+use near_vm_errors::{CompilationError, FunctionExecError};
 use near_vm_runner::VMError;
 
 /// Number of epochs it takes to unstake.
@@ -120,10 +120,10 @@ pub(crate) fn action_function_call(
     let code = match get_code_with_cache(state_update, account_id, &account) {
         Ok(Some(code)) => code,
         Ok(None) => {
-            let error = FunctionCallError::CompilationError(CompilationError::CodeDoesNotExist(
-                account_id.clone(),
+            let error = VMError::FunctionExecError(FunctionExecError::CompilationError(
+                CompilationError::CodeDoesNotExist { account_id: account_id.clone() },
             ));
-            result.result = Err(ActionError::FunctionCallError(error.to_string()));
+            result.result = Err(ActionErrorKind::FunctionCall(error).into());
             return Ok(());
         }
         Err(e) => {
@@ -181,8 +181,7 @@ pub(crate) fn action_function_call(
                 borsh::BorshDeserialize::try_from_slice(&storage).expect("Borsh cannot fail");
             return Err(err);
         }
-        // TODO(#1731): Handle VMError::FunctionCallError better.
-        result.result = Err(ActionError::FunctionCallError(err.to_string()));
+        result.result = Err(ActionErrorKind::FunctionCall(err).into());
         if let Some(outcome) = outcome {
             result.gas_burnt += outcome.burnt_gas;
             result.gas_burnt_for_function_call += outcome.burnt_gas;
@@ -218,7 +217,8 @@ pub(crate) fn action_stake(
     if account.amount >= increment {
         if account.locked == 0 && stake.stake == 0 {
             // if the account hasn't staked, it cannot unstake
-            result.result = Err(ActionError::TriesToUnstake(account_id.clone()));
+            result.result =
+                Err(ActionErrorKind::TriesToUnstake { account_id: account_id.clone() }.into());
             return;
         }
         result.validator_proposals.push(ValidatorStake {
@@ -231,12 +231,13 @@ pub(crate) fn action_stake(
             account.locked = stake.stake;
         }
     } else {
-        result.result = Err(ActionError::TriesToStake(
-            account_id.clone(),
-            stake.stake,
-            account.locked,
-            account.amount,
-        ));
+        result.result = Err(ActionErrorKind::TriesToStake {
+            account_id: account_id.clone(),
+            stake: stake.stake,
+            locked: account.locked,
+            balance: account.amount,
+        }
+        .into());
     }
 }
 
@@ -256,10 +257,11 @@ pub(crate) fn action_create_account(
     if !is_valid_top_level_account_id(account_id)
         && !is_valid_sub_account_id(&receipt.predecessor_id, account_id)
     {
-        result.result = Err(ActionError::CreateAccountNotAllowed(
-            account_id.clone(),
-            receipt.predecessor_id.clone(),
-        ));
+        result.result = Err(ActionErrorKind::CreateAccountNotAllowed {
+            account_id: account_id.clone(),
+            predecessor_id: receipt.predecessor_id.clone(),
+        }
+        .into());
         return;
     }
     *actor_id = receipt.receiver_id.clone();
@@ -322,7 +324,11 @@ pub(crate) fn action_delete_key(
     let account = account.as_mut().unwrap();
     let access_key = get_access_key(state_update, account_id, &delete_key.public_key)?;
     if access_key.is_none() {
-        result.result = Err(ActionError::DeleteKeyDoesNotExist(account_id.clone()));
+        result.result = Err(ActionErrorKind::DeleteKeyDoesNotExist {
+            public_key: delete_key.public_key.clone(),
+            account_id: account_id.clone(),
+        }
+        .into());
         return Ok(());
     }
     // Remove access key
@@ -349,7 +355,11 @@ pub(crate) fn action_add_key(
 ) -> Result<(), StorageError> {
     let account = account.as_mut().unwrap();
     if get_access_key(state_update, account_id, &add_key.public_key)?.is_some() {
-        result.result = Err(ActionError::AddKeyAlreadyExists(add_key.public_key.clone()));
+        result.result = Err(ActionErrorKind::AddKeyAlreadyExists {
+            account_id: account_id.to_owned(),
+            public_key: add_key.public_key.clone(),
+        }
+        .into());
         return Ok(());
     }
     set_access_key(state_update, account_id, &add_key.public_key, &add_key.access_key);
@@ -375,16 +385,19 @@ pub(crate) fn check_actor_permissions(
     match action {
         Action::DeployContract(_) | Action::Stake(_) | Action::AddKey(_) | Action::DeleteKey(_) => {
             if actor_id != account_id {
-                return Err(ActionError::ActorNoPermission(
-                    actor_id.clone(),
-                    account_id.clone(),
-                    action_type_as_string(action).to_owned(),
-                ));
+                return Err(ActionErrorKind::ActorNoPermission {
+                    account_id: actor_id.clone(),
+                    actor_id: account_id.clone(),
+                }
+                .into());
             }
         }
         Action::DeleteAccount(_) => {
             if account.as_ref().unwrap().locked != 0 {
-                return Err(ActionError::DeleteAccountStaking(account_id.clone()));
+                return Err(ActionErrorKind::DeleteAccountStaking {
+                    account_id: account_id.clone(),
+                }
+                .into());
             }
             if actor_id != account_id
                 && check_rent(
@@ -395,10 +408,11 @@ pub(crate) fn check_actor_permissions(
                 )
                 .is_ok()
             {
-                return Err(ActionError::DeleteAccountHasRent(
-                    account_id.clone(),
-                    account.as_ref().unwrap().amount,
-                ));
+                return Err(ActionErrorKind::DeleteAccountHasRent {
+                    account_id: account_id.clone(),
+                    balance: account.as_ref().unwrap().amount,
+                }
+                .into());
             }
         }
         Action::CreateAccount(_) | Action::FunctionCall(_) | Action::Transfer(_) => (),
@@ -406,19 +420,6 @@ pub(crate) fn check_actor_permissions(
     Ok(())
 }
 
-fn action_type_as_string(action: &Action) -> &'static str {
-    match action {
-        Action::CreateAccount(_) => "CreateAccount",
-        Action::DeployContract(_) => "DeployContract",
-        Action::FunctionCall(_) => "FunctionCall",
-        Action::Transfer(_) => "Transfer",
-        Action::Stake(_) => "Stake",
-        Action::AddKey(_) => "AddKey",
-        Action::DeleteKey(_) => "DeleteKey",
-        Action::DeleteAccount(_) => "DeleteAccount",
-    }
-}
-
 pub(crate) fn check_account_existence(
     action: &Action,
     account: &mut Option<Account>,
@@ -427,7 +428,10 @@ pub(crate) fn check_account_existence(
     match action {
         Action::CreateAccount(_) => {
             if account.is_some() {
-                return Err(ActionError::AccountAlreadyExists(account_id.clone()));
+                return Err(ActionErrorKind::AccountAlreadyExists {
+                    account_id: account_id.clone().into(),
+                }
+                .into());
             }
         }
         Action::DeployContract(_)
@@ -438,12 +442,11 @@ pub(crate) fn check_account_existence(
         | Action::DeleteKey(_)
         | Action::DeleteAccount(_) => {
             if account.is_none() {
-                return Err(ActionError::AccountDoesNotExist(
-                    action_type_as_string(action).to_owned(),
-                    account_id.clone(),
-                ));
+                return Err(ActionErrorKind::AccountDoesNotExist {
+                    account_id: account_id.clone(),
+                }
+                .into());
             }
-            //
         }
     };
     Ok(())
diff --git a/runtime/runtime/src/balance_checker.rs b/runtime/runtime/src/balance_checker.rs
index 22fbd2a0948..083b37c1962 100644
--- a/runtime/runtime/src/balance_checker.rs
+++ b/runtime/runtime/src/balance_checker.rs
@@ -278,7 +278,7 @@ mod tests {
             &ApplyStats::default(),
         )
         .unwrap_err();
-        assert_matches!(err, RuntimeError::BalanceMismatch(_));
+        assert_matches!(err, RuntimeError::BalanceMismatchError(_));
     }
 
     #[test]
diff --git a/runtime/runtime/src/ext.rs b/runtime/runtime/src/ext.rs
index d5a130d8bad..f5ce3a91b8f 100644
--- a/runtime/runtime/src/ext.rs
+++ b/runtime/runtime/src/ext.rs
@@ -166,7 +166,9 @@ impl<'a> External for RuntimeExt<'a> {
     ) -> ExtResult<Option<(Vec<u8>, Box<dyn ValuePtr + 'b>)>> {
         let result = match self.iters.get_mut(&iterator_idx) {
             Some(iter) => iter.next(),
-            None => return Err(HostError::InvalidIteratorIndex(iterator_idx).into()),
+            None => {
+                return Err(HostError::InvalidIteratorIndex { iterator_index: iterator_idx }.into())
+            }
         };
         match result {
             None => {
@@ -199,7 +201,7 @@ impl<'a> External for RuntimeExt<'a> {
                 .get_mut(receipt_index as usize)
                 .ok_or_else(|| {
                     HostErrorOrStorageError::HostError(
-                        HostError::InvalidReceiptIndex(receipt_index).into(),
+                        HostError::InvalidReceiptIndex { receipt_index }.into(),
                     )
                 })?
                 .1
diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs
index 6766e0f468a..097f7c89b3d 100644
--- a/runtime/runtime/src/lib.rs
+++ b/runtime/runtime/src/lib.rs
@@ -16,7 +16,8 @@ use near_crypto::PublicKey;
 use near_primitives::account::{AccessKey, AccessKeyPermission, Account};
 use near_primitives::contract::ContractCode;
 use near_primitives::errors::{
-    ActionError, ExecutionError, InvalidAccessKeyError, InvalidTxError, RuntimeError,
+    ActionError, ActionErrorKind, InvalidAccessKeyError, InvalidTxError, RuntimeError,
+    TxExecutionError,
 };
 use near_primitives::hash::CryptoHash;
 use near_primitives::receipt::{ActionReceipt, DataReceipt, Receipt, ReceiptEnum, ReceivedData};
@@ -218,10 +219,13 @@ impl Runtime {
         let transaction = &signed_transaction.transaction;
         let signer_id = &transaction.signer_id;
         if !is_valid_account_id(&signer_id) {
-            return Err(InvalidTxError::InvalidSigner(signer_id.clone()).into());
+            return Err(InvalidTxError::InvalidSignerId { signer_id: signer_id.clone() }.into());
         }
         if !is_valid_account_id(&transaction.receiver_id) {
-            return Err(InvalidTxError::InvalidReceiver(transaction.receiver_id.clone()).into());
+            return Err(InvalidTxError::InvalidReceiverId {
+                receiver_id: transaction.receiver_id.clone(),
+            }
+            .into());
         }
 
         if !signed_transaction
@@ -233,25 +237,31 @@ impl Runtime {
         let mut signer = match get_account(state_update, signer_id)? {
             Some(signer) => signer,
             None => {
-                return Err(InvalidTxError::SignerDoesNotExist(signer_id.clone()).into());
+                return Err(
+                    InvalidTxError::SignerDoesNotExist { signer_id: signer_id.clone() }.into()
+                );
             }
         };
         let mut access_key =
             match get_access_key(state_update, &signer_id, &transaction.public_key)? {
                 Some(access_key) => access_key,
                 None => {
-                    return Err(InvalidTxError::InvalidAccessKey(
-                        InvalidAccessKeyError::AccessKeyNotFound(
-                            signer_id.clone(),
-                            transaction.public_key.clone(),
-                        ),
+                    return Err(InvalidTxError::InvalidAccessKeyError(
+                        InvalidAccessKeyError::AccessKeyNotFound {
+                            account_id: signer_id.clone(),
+                            public_key: transaction.public_key.clone(),
+                        },
                     )
                     .into());
                 }
             };
 
         if transaction.nonce <= access_key.nonce {
-            return Err(InvalidTxError::InvalidNonce(transaction.nonce, access_key.nonce).into());
+            return Err(InvalidTxError::InvalidNonce {
+                tx_nonce: transaction.nonce,
+                ak_nonce: access_key.nonce,
+            }
+            .into());
         }
 
         let sender_is_receiver = &transaction.receiver_id == signer_id;
@@ -268,7 +278,11 @@ impl Runtime {
         .map_err(|_| InvalidTxError::CostOverflow)?;
 
         signer.amount = signer.amount.checked_sub(total_cost).ok_or_else(|| {
-            InvalidTxError::NotEnoughBalance(signer_id.clone(), signer.amount, total_cost)
+            InvalidTxError::NotEnoughBalance {
+                signer_id: signer_id.clone(),
+                balance: signer.amount,
+                cost: total_cost,
+            }
         })?;
 
         if let AccessKeyPermission::FunctionCall(ref mut function_call_permission) =
@@ -276,36 +290,39 @@ impl Runtime {
         {
             if let Some(ref mut allowance) = function_call_permission.allowance {
                 *allowance = allowance.checked_sub(total_cost).ok_or_else(|| {
-                    InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::NotEnoughAllowance(
-                        signer_id.clone(),
-                        transaction.public_key.clone(),
-                        *allowance,
-                        total_cost,
-                    ))
+                    InvalidTxError::InvalidAccessKeyError(
+                        InvalidAccessKeyError::NotEnoughAllowance {
+                            account_id: signer_id.clone(),
+                            public_key: transaction.public_key.clone(),
+                            allowance: *allowance,
+                            cost: total_cost,
+                        },
+                    )
                 })?;
             }
         }
 
         if let Err(amount) = check_rent(&signer_id, &signer, &self.config, apply_state.epoch_length)
         {
-            return Err(InvalidTxError::RentUnpaid(signer_id.clone(), amount).into());
+            return Err(InvalidTxError::RentUnpaid { signer_id: signer_id.clone(), amount }.into());
         }
 
         if let AccessKeyPermission::FunctionCall(ref function_call_permission) =
             access_key.permission
         {
             if transaction.actions.len() != 1 {
-                return Err(
-                    InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::ActionError).into()
-                );
+                return Err(InvalidTxError::InvalidAccessKeyError(
+                    InvalidAccessKeyError::RequiresFullAccess,
+                )
+                .into());
             }
             if let Some(Action::FunctionCall(ref function_call)) = transaction.actions.get(0) {
                 if transaction.receiver_id != function_call_permission.receiver_id {
-                    return Err(InvalidTxError::InvalidAccessKey(
-                        InvalidAccessKeyError::ReceiverMismatch(
-                            transaction.receiver_id.clone(),
-                            function_call_permission.receiver_id.clone(),
-                        ),
+                    return Err(InvalidTxError::InvalidAccessKeyError(
+                        InvalidAccessKeyError::ReceiverMismatch {
+                            tx_receiver: transaction.receiver_id.clone(),
+                            ak_receiver: function_call_permission.receiver_id.clone(),
+                        },
                     )
                     .into());
                 }
@@ -315,17 +332,18 @@ impl Runtime {
                         .iter()
                         .all(|method_name| &function_call.method_name != method_name)
                 {
-                    return Err(InvalidTxError::InvalidAccessKey(
-                        InvalidAccessKeyError::MethodNameMismatch(
-                            function_call.method_name.clone(),
-                        ),
+                    return Err(InvalidTxError::InvalidAccessKeyError(
+                        InvalidAccessKeyError::MethodNameMismatch {
+                            method_name: function_call.method_name.clone(),
+                        },
                     )
                     .into());
                 }
             } else {
-                return Err(
-                    InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::ActionError).into()
-                );
+                return Err(InvalidTxError::InvalidAccessKeyError(
+                    InvalidAccessKeyError::RequiresFullAccess,
+                )
+                .into());
             }
         };
 
@@ -598,7 +616,8 @@ impl Runtime {
                 is_last_action,
             )?)?;
             // TODO storage error
-            if result.result.is_err() {
+            if let Err(ref mut res) = result.result {
+                res.index = Some(action_index as u64);
                 break;
             }
         }
@@ -610,7 +629,13 @@ impl Runtime {
                     check_rent(account_id, account, &self.config, apply_state.epoch_length)
                 {
                     result.merge(ActionResult {
-                        result: Err(ActionError::RentUnpaid(account_id.clone(), amount)),
+                        result: Err(ActionError {
+                            index: None,
+                            kind: ActionErrorKind::RentUnpaid {
+                                account_id: account_id.clone(),
+                                amount,
+                            },
+                        }),
                         ..Default::default()
                     })?;
                 } else {
@@ -745,7 +770,7 @@ impl Runtime {
             ),
             Ok(ReturnData::Value(data)) => ExecutionStatus::SuccessValue(data),
             Ok(ReturnData::None) => ExecutionStatus::SuccessValue(vec![]),
-            Err(e) => ExecutionStatus::Failure(ExecutionError::Action(e)),
+            Err(e) => ExecutionStatus::Failure(TxExecutionError::ActionError(e)),
         };
 
         Self::print_log(&result.logs);
diff --git a/runtime/runtime/tests/test_evil_contracts.rs b/runtime/runtime/tests/test_evil_contracts.rs
index 457bc4b5ac9..e74f41eb0f9 100644
--- a/runtime/runtime/tests/test_evil_contracts.rs
+++ b/runtime/runtime/tests/test_evil_contracts.rs
@@ -1,6 +1,7 @@
-use near_primitives::errors::ActionError;
+use near_primitives::errors::{ActionError, ActionErrorKind};
 use near_primitives::serialize::to_base64;
 use near_primitives::views::FinalExecutionStatus;
+use near_vm_errors::{FunctionExecError, HostError, VMError};
 use std::mem::size_of;
 use testlib::node::{Node, RuntimeNode};
 
@@ -134,8 +135,13 @@ fn test_evil_abort() {
     assert_eq!(
         res.status,
         FinalExecutionStatus::Failure(
-            ActionError::FunctionCallError("String encoding is bad UTF-16 sequence.".to_string())
-                .into()
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::FunctionCall(VMError::FunctionExecError(
+                    FunctionExecError::HostError(HostError::BadUTF16)
+                ))
+            }
+            .into()
         ),
         "{:?}",
         res
diff --git a/scripts/parallel_run_tests.py b/scripts/parallel_run_tests.py
index 6879fe47876..695e9c75362 100644
--- a/scripts/parallel_run_tests.py
+++ b/scripts/parallel_run_tests.py
@@ -17,7 +17,7 @@ def show_test_result(binary, result):
 if __name__ == "__main__":
     clean_binary_tests()
     build_tests()
-    binaries = test_binaries(exclude=[r'test_regression-.*'])
+    binaries = test_binaries(exclude=[r'test_regression-.*', r'near_rpc_error_macro-.*'])
     print(f'========= collected {len(binaries)} test binaries:')
     print('\n'.join(binaries))
 
diff --git a/scripts/test_nearlib.sh b/scripts/test_nearlib.sh
index 63e87089291..92125db7a17 100755
--- a/scripts/test_nearlib.sh
+++ b/scripts/test_nearlib.sh
@@ -21,9 +21,10 @@ function get_nearlib_nearshell_release () {
 
 function get_nearlib_nearshell_git () {
     rm -rf nearlib
-    git clone --single-branch --branch master https://github.com/nearprotocol/nearlib.git nearlib
-    rm -rf near-shell
-    git clone --single-branch --branch master https://git@github.com/nearprotocol/near-shell.git near-shell
+    git clone https://github.com/nearprotocol/nearlib.git nearlib
+    cd nearlib
+    git checkout 436ed6339337e11cc4aca79cd43dd5f27feadd39
+    cd ..
 }
 
 if [ -z "${NEARLIB_RELEASE}" ]; then
@@ -39,10 +40,3 @@ yarn build
 ../scripts/waitonserver.sh
 yarn test
 yarn doc
-cd ..
-
-# Try creating and building new project using NEAR CLI tools
-cd near-shell
-yarn
-#yarn test
-cd ..
diff --git a/test-utils/testlib/Cargo.toml b/test-utils/testlib/Cargo.toml
index 433c36541f1..368af4d09b9 100644
--- a/test-utils/testlib/Cargo.toml
+++ b/test-utils/testlib/Cargo.toml
@@ -26,6 +26,7 @@ near-crypto = { path = "../../core/crypto" }
 near-primitives = { path = "../../core/primitives" }
 near-store = { path = "../../core/store" }
 node-runtime = { path = "../../runtime/runtime" }
+near-vm-errors = { path = "../../runtime/near-vm-errors" }
 near-chain = { path = "../../chain/chain" }
 near-client = { path = "../../chain/client" }
 near-jsonrpc = { path = "../../chain/jsonrpc" }
diff --git a/test-utils/testlib/src/node/mod.rs b/test-utils/testlib/src/node/mod.rs
index d466afbbe73..419af12c46d 100644
--- a/test-utils/testlib/src/node/mod.rs
+++ b/test-utils/testlib/src/node/mod.rs
@@ -9,6 +9,7 @@ use near::config::{
 };
 use near::NearConfig;
 use near_crypto::{InMemorySigner, Signer};
+use near_jsonrpc::ServerError;
 use near_primitives::serialize::to_base64;
 use near_primitives::transaction::SignedTransaction;
 use near_primitives::types::{AccountId, Balance, NumSeats};
@@ -66,7 +67,7 @@ pub trait Node: Send + Sync {
         self.user().view_balance(account_id)
     }
 
-    fn add_transaction(&self, transaction: SignedTransaction) -> Result<(), String> {
+    fn add_transaction(&self, transaction: SignedTransaction) -> Result<(), ServerError> {
         self.user().add_transaction(transaction)
     }
 
diff --git a/test-utils/testlib/src/standard_test_cases.rs b/test-utils/testlib/src/standard_test_cases.rs
index 62c7564ba13..20f87ff0f1e 100644
--- a/test-utils/testlib/src/standard_test_cases.rs
+++ b/test-utils/testlib/src/standard_test_cases.rs
@@ -2,13 +2,17 @@ use std::sync::Arc;
 
 use near::config::{TESTING_INIT_BALANCE, TESTING_INIT_STAKE};
 use near_crypto::{InMemorySigner, KeyType};
+use near_jsonrpc::ServerError;
 use near_primitives::account::{AccessKey, AccessKeyPermission, FunctionCallPermission};
-use near_primitives::errors::{ActionError, InvalidAccessKeyError, InvalidTxError};
+use near_primitives::errors::{
+    ActionError, ActionErrorKind, InvalidAccessKeyError, InvalidTxError, TxExecutionError,
+};
 use near_primitives::hash::hash;
 use near_primitives::serialize::to_base64;
 use near_primitives::types::Balance;
 use near_primitives::views::FinalExecutionStatus;
 use near_primitives::views::{AccountView, FinalExecutionOutcomeView};
+use near_vm_errors::{FunctionExecError, HostError, MethodResolveError, VMError};
 
 use crate::fees_utils::FeeHelper;
 use crate::node::Node;
@@ -76,7 +80,15 @@ pub fn test_smart_contract_panic(node: impl Node) {
     assert_eq!(
         transaction_result.status,
         FinalExecutionStatus::Failure(
-            ActionError::FunctionCallError("Smart contract panicked: WAT?".to_string()).into()
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::FunctionCall(VMError::FunctionExecError(
+                    FunctionExecError::HostError(HostError::GuestPanic {
+                        panic_msg: "WAT?".to_string()
+                    })
+                ))
+            }
+            .into()
         )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 2);
@@ -108,7 +120,13 @@ pub fn test_smart_contract_bad_method_name(node: impl Node) {
     assert_eq!(
         transaction_result.status,
         FinalExecutionStatus::Failure(
-            ActionError::FunctionCallError("MethodNotFound".to_string()).into()
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::FunctionCall(VMError::FunctionExecError(
+                    FunctionExecError::MethodResolveError(MethodResolveError::MethodNotFound)
+                ))
+            }
+            .into()
         )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 2);
@@ -126,7 +144,13 @@ pub fn test_smart_contract_empty_method_name_with_no_tokens(node: impl Node) {
     assert_eq!(
         transaction_result.status,
         FinalExecutionStatus::Failure(
-            ActionError::FunctionCallError("MethodEmptyName".to_string()).into()
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::FunctionCall(VMError::FunctionExecError(
+                    FunctionExecError::MethodResolveError(MethodResolveError::MethodEmptyName)
+                ))
+            }
+            .into()
         )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 2);
@@ -144,7 +168,13 @@ pub fn test_smart_contract_empty_method_name_with_tokens(node: impl Node) {
     assert_eq!(
         transaction_result.status,
         FinalExecutionStatus::Failure(
-            ActionError::FunctionCallError("MethodEmptyName".to_string()).into()
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::FunctionCall(VMError::FunctionExecError(
+                    FunctionExecError::MethodResolveError(MethodResolveError::MethodEmptyName)
+                ))
+            }
+            .into()
         )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 2);
@@ -345,8 +375,11 @@ pub fn test_refund_on_send_money_to_non_existent_account(node: impl Node) {
     assert_eq!(
         transaction_result.status,
         FinalExecutionStatus::Failure(
-            ActionError::AccountDoesNotExist("Transfer".to_string(), eve_dot_alice_account())
-                .into()
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::AccountDoesNotExist { account_id: eve_dot_alice_account() }
+            }
+            .into()
         )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 2);
@@ -437,7 +470,11 @@ pub fn test_create_account_again(node: impl Node) {
     assert_eq!(
         transaction_result.status,
         FinalExecutionStatus::Failure(
-            ActionError::AccountAlreadyExists(eve_dot_alice_account()).into()
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::AccountAlreadyExists { account_id: eve_dot_alice_account() }
+            }
+            .into()
         )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 2);
@@ -477,7 +514,9 @@ pub fn test_create_account_failure_invalid_name(node: impl Node) {
             .unwrap_err();
         assert_eq!(
             transaction_result,
-            format!("{}", InvalidTxError::InvalidReceiver(invalid_account_name.to_string()))
+            ServerError::TxExecutionError(TxExecutionError::InvalidTxError(
+                InvalidTxError::InvalidReceiverId { receiver_id: invalid_account_name.to_string() }
+            ))
         );
     }
 }
@@ -496,7 +535,13 @@ pub fn test_create_account_failure_already_exists(node: impl Node) {
         fee_helper.create_account_transfer_full_key_cost_fail_on_create_account();
     assert_eq!(
         transaction_result.status,
-        FinalExecutionStatus::Failure(ActionError::AccountAlreadyExists(bob_account()).into())
+        FinalExecutionStatus::Failure(
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::AccountAlreadyExists { account_id: bob_account() }
+            }
+            .into()
+        )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 2);
     let new_root = node_user.get_state_root();
@@ -573,7 +618,14 @@ pub fn test_add_existing_key(node: impl Node) {
     assert_eq!(
         transaction_result.status,
         FinalExecutionStatus::Failure(
-            ActionError::AddKeyAlreadyExists(node.signer().public_key()).into()
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::AddKeyAlreadyExists {
+                    account_id: account_id.clone(),
+                    public_key: node.signer().public_key()
+                }
+            }
+            .into()
         )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 1);
@@ -618,7 +670,14 @@ pub fn test_delete_key_not_owned(node: impl Node) {
     assert_eq!(
         transaction_result.status,
         FinalExecutionStatus::Failure(
-            ActionError::DeleteKeyDoesNotExist(account_id.clone()).into()
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::DeleteKeyDoesNotExist {
+                    account_id: account_id.clone(),
+                    public_key: signer2.public_key.clone()
+                }
+            }
+            .into()
         )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 1);
@@ -824,12 +883,11 @@ pub fn test_access_key_smart_contract_reject_method_name(node: impl Node) {
         .unwrap_err();
     assert_eq!(
         transaction_result,
-        format!(
-            "{}",
-            InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::MethodNameMismatch(
-                "run_test".to_string()
-            ))
-        )
+        ServerError::TxExecutionError(TxExecutionError::InvalidTxError(
+            InvalidTxError::InvalidAccessKeyError(InvalidAccessKeyError::MethodNameMismatch {
+                method_name: "run_test".to_string()
+            })
+        ))
     );
 }
 
@@ -860,13 +918,12 @@ pub fn test_access_key_smart_contract_reject_contract_id(node: impl Node) {
         .unwrap_err();
     assert_eq!(
         transaction_result,
-        format!(
-            "{}",
-            InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::ReceiverMismatch(
-                eve_dot_alice_account(),
-                bob_account(),
-            ))
-        )
+        ServerError::TxExecutionError(TxExecutionError::InvalidTxError(
+            InvalidTxError::InvalidAccessKeyError(InvalidAccessKeyError::ReceiverMismatch {
+                tx_receiver: eve_dot_alice_account(),
+                ak_receiver: bob_account()
+            })
+        ))
     );
 }
 
@@ -889,7 +946,9 @@ pub fn test_access_key_reject_non_function_call(node: impl Node) {
         node_user.delete_key(account_id.clone(), node.signer().public_key()).unwrap_err();
     assert_eq!(
         transaction_result,
-        format!("{}", InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::ActionError))
+        ServerError::TxExecutionError(TxExecutionError::InvalidTxError(
+            InvalidTxError::InvalidAccessKeyError(InvalidAccessKeyError::RequiresFullAccess)
+        ))
     );
 }
 
@@ -950,7 +1009,13 @@ pub fn test_unstake_while_not_staked(node: impl Node) {
         node_user.stake(eve_dot_alice_account(), node.block_signer().public_key(), 0).unwrap();
     assert_eq!(
         transaction_result.status,
-        FinalExecutionStatus::Failure(ActionError::TriesToUnstake(eve_dot_alice_account()).into())
+        FinalExecutionStatus::Failure(
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::TriesToUnstake { account_id: eve_dot_alice_account() }
+            }
+            .into()
+        )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 1);
 }
@@ -983,7 +1048,14 @@ fn test_stake_fail_not_enough_rent_with_balance(node: impl Node, initial_balance
         node_user.stake(new_account_id.clone(), node.block_signer().public_key(), 5).unwrap();
     assert_matches!(
         &transaction_result.status,
-        FinalExecutionStatus::Failure(e) if e.error_type == "ActionError::RentUnpaid"
+        FinalExecutionStatus::Failure(e) => match &e {
+            &TxExecutionError::ActionError(action_err) =>
+                match action_err.kind {
+                    ActionErrorKind::RentUnpaid{..} => {},
+                    _ => panic!("should be RentUnpaid")
+                }
+            _ => panic!("should be Action")
+        }
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 1);
 }
@@ -1025,7 +1097,13 @@ pub fn test_delete_account_fail(node: impl Node) {
     let transaction_result = node_user.delete_account(alice_account(), bob_account()).unwrap();
     assert_eq!(
         transaction_result.status,
-        FinalExecutionStatus::Failure(ActionError::DeleteAccountStaking(bob_account()).into())
+        FinalExecutionStatus::Failure(
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::DeleteAccountStaking { account_id: bob_account() }
+            }
+            .into()
+        )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 1);
     assert!(node.user().view_account(&bob_account()).is_ok());
@@ -1042,8 +1120,11 @@ pub fn test_delete_account_no_account(node: impl Node) {
     assert_eq!(
         transaction_result.status,
         FinalExecutionStatus::Failure(
-            ActionError::AccountDoesNotExist("DeleteAccount".to_string(), eve_dot_alice_account())
-                .into()
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::AccountDoesNotExist { account_id: eve_dot_alice_account() }
+            }
+            .into()
         )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 1);
@@ -1070,7 +1151,11 @@ pub fn test_delete_account_while_staking(node: impl Node) {
     assert_eq!(
         transaction_result.status,
         FinalExecutionStatus::Failure(
-            ActionError::DeleteAccountStaking(eve_dot_alice_account()).into()
+            ActionError {
+                index: Some(0),
+                kind: ActionErrorKind::DeleteAccountStaking { account_id: eve_dot_alice_account() }
+            }
+            .into()
         )
     );
     assert_eq!(transaction_result.receipts_outcome.len(), 1);
diff --git a/test-utils/testlib/src/user/mod.rs b/test-utils/testlib/src/user/mod.rs
index 9deca9f1d60..98b2702f56e 100644
--- a/test-utils/testlib/src/user/mod.rs
+++ b/test-utils/testlib/src/user/mod.rs
@@ -3,6 +3,7 @@ use std::sync::Arc;
 use futures::{future::LocalBoxFuture, FutureExt};
 
 use near_crypto::{PublicKey, Signer};
+use near_jsonrpc::ServerError;
 use near_primitives::account::AccessKey;
 use near_primitives::hash::CryptoHash;
 use near_primitives::receipt::Receipt;
@@ -31,14 +32,14 @@ pub trait User {
 
     fn view_state(&self, account_id: &AccountId, prefix: &[u8]) -> Result<ViewStateResult, String>;
 
-    fn add_transaction(&self, signed_transaction: SignedTransaction) -> Result<(), String>;
+    fn add_transaction(&self, signed_transaction: SignedTransaction) -> Result<(), ServerError>;
 
     fn commit_transaction(
         &self,
         signed_transaction: SignedTransaction,
-    ) -> Result<FinalExecutionOutcomeView, String>;
+    ) -> Result<FinalExecutionOutcomeView, ServerError>;
 
-    fn add_receipt(&self, receipt: Receipt) -> Result<(), String>;
+    fn add_receipt(&self, receipt: Receipt) -> Result<(), ServerError>;
 
     fn get_access_key_nonce_for_signer(&self, account_id: &AccountId) -> Result<u64, String> {
         self.get_access_key(account_id, &self.signer().public_key())
@@ -72,7 +73,7 @@ pub trait User {
         signer_id: AccountId,
         receiver_id: AccountId,
         actions: Vec<Action>,
-    ) -> Result<FinalExecutionOutcomeView, String> {
+    ) -> Result<FinalExecutionOutcomeView, ServerError> {
         let block_hash = self.get_best_block_hash().unwrap_or(CryptoHash::default());
         let signed_transaction = SignedTransaction::from_actions(
             self.get_access_key_nonce_for_signer(&signer_id).unwrap_or_default() + 1,
@@ -90,7 +91,7 @@ pub trait User {
         signer_id: AccountId,
         receiver_id: AccountId,
         amount: Balance,
-    ) -> Result<FinalExecutionOutcomeView, String> {
+    ) -> Result<FinalExecutionOutcomeView, ServerError> {
         self.sign_and_commit_actions(
             signer_id,
             receiver_id,
@@ -102,7 +103,7 @@ pub trait User {
         &self,
         signer_id: AccountId,
         code: Vec<u8>,
-    ) -> Result<FinalExecutionOutcomeView, String> {
+    ) -> Result<FinalExecutionOutcomeView, ServerError> {
         self.sign_and_commit_actions(
             signer_id.clone(),
             signer_id,
@@ -118,7 +119,7 @@ pub trait User {
         args: Vec<u8>,
         gas: Gas,
         deposit: Balance,
-    ) -> Result<FinalExecutionOutcomeView, String> {
+    ) -> Result<FinalExecutionOutcomeView, ServerError> {
         self.sign_and_commit_actions(
             signer_id,
             contract_id,
@@ -137,7 +138,7 @@ pub trait User {
         new_account_id: AccountId,
         public_key: PublicKey,
         amount: Balance,
-    ) -> Result<FinalExecutionOutcomeView, String> {
+    ) -> Result<FinalExecutionOutcomeView, ServerError> {
         self.sign_and_commit_actions(
             signer_id,
             new_account_id,
@@ -154,7 +155,7 @@ pub trait User {
         signer_id: AccountId,
         public_key: PublicKey,
         access_key: AccessKey,
-    ) -> Result<FinalExecutionOutcomeView, String> {
+    ) -> Result<FinalExecutionOutcomeView, ServerError> {
         self.sign_and_commit_actions(
             signer_id.clone(),
             signer_id,
@@ -166,7 +167,7 @@ pub trait User {
         &self,
         signer_id: AccountId,
         public_key: PublicKey,
-    ) -> Result<FinalExecutionOutcomeView, String> {
+    ) -> Result<FinalExecutionOutcomeView, ServerError> {
         self.sign_and_commit_actions(
             signer_id.clone(),
             signer_id,
@@ -180,7 +181,7 @@ pub trait User {
         old_public_key: PublicKey,
         new_public_key: PublicKey,
         access_key: AccessKey,
-    ) -> Result<FinalExecutionOutcomeView, String> {
+    ) -> Result<FinalExecutionOutcomeView, ServerError> {
         self.sign_and_commit_actions(
             signer_id.clone(),
             signer_id,
@@ -195,7 +196,7 @@ pub trait User {
         &self,
         signer_id: AccountId,
         receiver_id: AccountId,
-    ) -> Result<FinalExecutionOutcomeView, String> {
+    ) -> Result<FinalExecutionOutcomeView, ServerError> {
         self.sign_and_commit_actions(
             signer_id.clone(),
             receiver_id,
@@ -208,7 +209,7 @@ pub trait User {
         signer_id: AccountId,
         public_key: PublicKey,
         stake: Balance,
-    ) -> Result<FinalExecutionOutcomeView, String> {
+    ) -> Result<FinalExecutionOutcomeView, ServerError> {
         self.sign_and_commit_actions(
             signer_id.clone(),
             signer_id,
@@ -222,49 +223,49 @@ pub trait AsyncUser: Send + Sync {
     fn view_account(
         &self,
         account_id: &AccountId,
-    ) -> LocalBoxFuture<'static, Result<AccountView, String>>;
+    ) -> LocalBoxFuture<'static, Result<AccountView, ServerError>>;
 
     fn view_balance(
         &self,
         account_id: &AccountId,
-    ) -> LocalBoxFuture<'static, Result<Balance, String>> {
+    ) -> LocalBoxFuture<'static, Result<Balance, ServerError>> {
         self.view_account(account_id).map(|res| res.map(|acc| acc.amount)).boxed_local()
     }
 
     fn view_state(
         &self,
         account_id: &AccountId,
-    ) -> LocalBoxFuture<'static, Result<ViewStateResult, String>>;
+    ) -> LocalBoxFuture<'static, Result<ViewStateResult, ServerError>>;
 
     fn add_transaction(
         &self,
         transaction: SignedTransaction,
-    ) -> LocalBoxFuture<'static, Result<(), String>>;
+    ) -> LocalBoxFuture<'static, Result<(), ServerError>>;
 
-    fn add_receipt(&self, receipt: Receipt) -> LocalBoxFuture<'static, Result<(), String>>;
+    fn add_receipt(&self, receipt: Receipt) -> LocalBoxFuture<'static, Result<(), ServerError>>;
 
     fn get_account_nonce(
         &self,
         account_id: &AccountId,
-    ) -> LocalBoxFuture<'static, Result<u64, String>>;
+    ) -> LocalBoxFuture<'static, Result<u64, ServerError>>;
 
-    fn get_best_height(&self) -> LocalBoxFuture<'static, Result<BlockHeight, String>>;
+    fn get_best_height(&self) -> LocalBoxFuture<'static, Result<BlockHeight, ServerError>>;
 
     fn get_transaction_result(
         &self,
         hash: &CryptoHash,
-    ) -> LocalBoxFuture<'static, Result<ExecutionOutcome, String>>;
+    ) -> LocalBoxFuture<'static, Result<ExecutionOutcome, ServerError>>;
 
     fn get_transaction_final_result(
         &self,
         hash: &CryptoHash,
-    ) -> LocalBoxFuture<'static, Result<FinalExecutionOutcomeView, String>>;
+    ) -> LocalBoxFuture<'static, Result<FinalExecutionOutcomeView, ServerError>>;
 
-    fn get_state_root(&self) -> LocalBoxFuture<'static, Result<MerkleHash, String>>;
+    fn get_state_root(&self) -> LocalBoxFuture<'static, Result<MerkleHash, ServerError>>;
 
     fn get_access_key(
         &self,
         account_id: &AccountId,
         public_key: &PublicKey,
-    ) -> LocalBoxFuture<'static, Result<Option<AccessKey>, String>>;
+    ) -> LocalBoxFuture<'static, Result<Option<AccessKey>, ServerError>>;
 }
diff --git a/test-utils/testlib/src/user/rpc_user.rs b/test-utils/testlib/src/user/rpc_user.rs
index b5683120548..39055441e99 100644
--- a/test-utils/testlib/src/user/rpc_user.rs
+++ b/test-utils/testlib/src/user/rpc_user.rs
@@ -10,14 +10,15 @@ use futures::{Future, TryFutureExt};
 use near_client::StatusResponse;
 use near_crypto::{PublicKey, Signer};
 use near_jsonrpc::client::{new_client, JsonRpcClient};
+use near_jsonrpc::ServerError;
 use near_primitives::hash::CryptoHash;
 use near_primitives::receipt::Receipt;
 use near_primitives::serialize::{to_base, to_base64};
 use near_primitives::transaction::SignedTransaction;
 use near_primitives::types::{AccountId, BlockHeight, BlockId, MaybeBlockId};
 use near_primitives::views::{
-    AccessKeyView, AccountView, BlockView, EpochValidatorInfo, ExecutionErrorView,
-    ExecutionOutcomeView, FinalExecutionOutcomeView, QueryResponse, ViewStateResult,
+    AccessKeyView, AccountView, BlockView, EpochValidatorInfo, ExecutionOutcomeView,
+    FinalExecutionOutcomeView, QueryResponse, ViewStateResult,
 };
 
 use crate::user::User;
@@ -66,18 +67,23 @@ impl User for RpcUser {
         self.query(format!("contract/{}", account_id), prefix)?.try_into()
     }
 
-    fn add_transaction(&self, transaction: SignedTransaction) -> Result<(), String> {
+    fn add_transaction(&self, transaction: SignedTransaction) -> Result<(), ServerError> {
         let bytes = transaction.try_to_vec().unwrap();
         let _ = self
             .actix(move |mut client| client.broadcast_tx_async(to_base64(&bytes)))
-            .map_err(|err| err.to_string())?;
+            .map_err(|err| {
+                serde_json::from_value::<ServerError>(
+                    err.data.expect("server error must carry data"),
+                )
+                .expect("deserialize server error must be ok")
+            })?;
         Ok(())
     }
 
     fn commit_transaction(
         &self,
         transaction: SignedTransaction,
-    ) -> Result<FinalExecutionOutcomeView, String> {
+    ) -> Result<FinalExecutionOutcomeView, ServerError> {
         let bytes = transaction.try_to_vec().unwrap();
         let result = self.actix(move |mut client| client.broadcast_tx_commit(to_base64(&bytes)));
         // Wait for one more block, to make sure all nodes actually apply the state transition.
@@ -87,16 +93,11 @@ impl User for RpcUser {
         }
         match result {
             Ok(outcome) => Ok(outcome),
-            Err(err) => {
-                match serde_json::from_value::<ExecutionErrorView>(err.clone().data.unwrap()) {
-                    Ok(error_view) => Err(error_view.error_message),
-                    Err(_) => Err(serde_json::to_string(&err).unwrap()),
-                }
-            }
+            Err(err) => Err(serde_json::from_value::<ServerError>(err.data.unwrap()).unwrap()),
         }
     }
 
-    fn add_receipt(&self, _receipt: Receipt) -> Result<(), String> {
+    fn add_receipt(&self, _receipt: Receipt) -> Result<(), ServerError> {
         // TDDO: figure out if rpc will support this
         unimplemented!()
     }
diff --git a/test-utils/testlib/src/user/runtime_user.rs b/test-utils/testlib/src/user/runtime_user.rs
index c5877d13aad..d33714e8bc7 100644
--- a/test-utils/testlib/src/user/runtime_user.rs
+++ b/test-utils/testlib/src/user/runtime_user.rs
@@ -3,7 +3,8 @@ use std::collections::{HashMap, HashSet};
 use std::sync::{Arc, RwLock};
 
 use near_crypto::{PublicKey, Signer};
-use near_primitives::errors::RuntimeError;
+use near_jsonrpc::ServerError;
+use near_primitives::errors::{RuntimeError, TxExecutionError};
 use near_primitives::hash::CryptoHash;
 use near_primitives::receipt::Receipt;
 use near_primitives::transaction::SignedTransaction;
@@ -66,7 +67,7 @@ impl RuntimeUser {
         apply_state: ApplyState,
         prev_receipts: Vec<Receipt>,
         transactions: Vec<SignedTransaction>,
-    ) -> Result<(), String> {
+    ) -> Result<(), ServerError> {
         let mut receipts = prev_receipts;
         for transaction in transactions.iter() {
             self.transactions.borrow_mut().insert(transaction.clone());
@@ -86,8 +87,10 @@ impl RuntimeUser {
                     &HashSet::new(),
                 )
                 .map_err(|e| match e {
-                    RuntimeError::InvalidTxError(e) => format!("{}", e),
-                    RuntimeError::BalanceMismatch(e) => panic!("{}", e),
+                    RuntimeError::InvalidTxError(e) => {
+                        ServerError::TxExecutionError(TxExecutionError::InvalidTxError(e))
+                    }
+                    RuntimeError::BalanceMismatchError(e) => panic!("{}", e),
                     RuntimeError::StorageError(e) => panic!("Storage error {:?}", e),
                     RuntimeError::UnexpectedIntegerOverflow => {
                         panic!("UnexpectedIntegerOverflow error")
@@ -197,7 +200,7 @@ impl User for RuntimeUser {
             .map_err(|err| err.to_string())
     }
 
-    fn add_transaction(&self, transaction: SignedTransaction) -> Result<(), String> {
+    fn add_transaction(&self, transaction: SignedTransaction) -> Result<(), ServerError> {
         self.apply_all(self.apply_state(), vec![], vec![transaction])?;
         Ok(())
     }
@@ -205,12 +208,12 @@ impl User for RuntimeUser {
     fn commit_transaction(
         &self,
         transaction: SignedTransaction,
-    ) -> Result<FinalExecutionOutcomeView, String> {
+    ) -> Result<FinalExecutionOutcomeView, ServerError> {
         self.apply_all(self.apply_state(), vec![], vec![transaction.clone()])?;
         Ok(self.get_transaction_final_result(&transaction.get_hash()))
     }
 
-    fn add_receipt(&self, receipt: Receipt) -> Result<(), String> {
+    fn add_receipt(&self, receipt: Receipt) -> Result<(), ServerError> {
         self.apply_all(self.apply_state(), vec![receipt], vec![])?;
         Ok(())
     }
diff --git a/tests/test_errors.rs b/tests/test_errors.rs
index 8bacd81c1ef..9c39ce2e0ab 100644
--- a/tests/test_errors.rs
+++ b/tests/test_errors.rs
@@ -47,13 +47,11 @@ fn test_check_tx_error_log() {
     let tx_result = node.user().commit_transaction(tx).unwrap_err();
     assert_eq!(
         tx_result,
-        format!(
-            "{}",
-            InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::AccessKeyNotFound(
-                "bob.near".to_string(),
-                signer.public_key.clone()
-            ))
-        ),
+        InvalidTxError::InvalidAccessKeyError(InvalidAccessKeyError::AccessKeyNotFound {
+            account_id: "bob.near".to_string(),
+            public_key: signer.public_key.clone()
+        })
+        .into()
     );
 }
 
@@ -86,13 +84,11 @@ fn test_deliver_tx_error_log() {
     let tx_result = node.user().commit_transaction(tx).unwrap_err();
     assert_eq!(
         tx_result,
-        format!(
-            "{}",
-            InvalidTxError::NotEnoughBalance(
-                "alice.near".to_string(),
-                TESTING_INIT_BALANCE - TESTING_INIT_STAKE,
-                TESTING_INIT_BALANCE + 1 + cost
-            )
-        ),
+        InvalidTxError::NotEnoughBalance {
+            signer_id: "alice.near".to_string(),
+            balance: TESTING_INIT_BALANCE - TESTING_INIT_STAKE,
+            cost: TESTING_INIT_BALANCE + 1 + cost
+        }
+        .into()
     );
 }
diff --git a/tools/rpctypegen/core/Cargo.toml b/tools/rpctypegen/core/Cargo.toml
new file mode 100644
index 00000000000..9ce6952bee1
--- /dev/null
+++ b/tools/rpctypegen/core/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "near-rpc-error-core"
+version = "0.1.0"
+authors = ["Near Inc <hello@nearprotocol.com>"]
+edition = "2018"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = {version = "1.0", features = ["preserve_order"]}
+syn = { version = "1.0", features = ["full", "extra-traits"]}
+quote = "1.0"
+proc-macro2 = "1.0"
+
+[features]
+test = []
+dump_errors_schema = []
diff --git a/tools/rpctypegen/core/src/lib.rs b/tools/rpctypegen/core/src/lib.rs
new file mode 100644
index 00000000000..b2f64b9b1e0
--- /dev/null
+++ b/tools/rpctypegen/core/src/lib.rs
@@ -0,0 +1,303 @@
+extern crate proc_macro;
+extern crate proc_macro2;
+
+use std::collections::BTreeMap;
+use syn::{Data, DataEnum, DataStruct, DeriveInput, Fields, FieldsNamed, FieldsUnnamed};
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Default, Debug, Deserialize, Serialize)]
+pub struct ErrorType {
+    /// A type name of the error
+    pub name: String,
+    /// Names of subtypes of the error
+    pub subtypes: Vec<String>,
+    /// An error input name and a type
+    pub props: BTreeMap<String, String>,
+}
+
+fn parse_rpc_error_variant(input: &DeriveInput) -> String {
+    let type_name = input.ident.to_string();
+    let type_kind: Vec<&str> = type_name.split("Kind").collect();
+    type_kind[0].to_string()
+}
+
+fn error_type_name<'a>(
+    schema: &'a mut BTreeMap<String, ErrorType>,
+    name: String,
+) -> &'a mut ErrorType {
+    let error_type = ErrorType { name: name.clone(), ..Default::default() };
+    schema.entry(name.clone()).or_insert(error_type)
+}
+
+pub fn parse_error_type(schema: &mut BTreeMap<String, ErrorType>, input: &DeriveInput) {
+    let name = parse_rpc_error_variant(input);
+    match &input.data {
+        Data::Enum(DataEnum { ref variants, .. }) => {
+            // TODO: check for uniqueness
+            let error_type = error_type_name(schema, name);
+            let mut direct_error_types = vec![];
+            for variant in variants {
+                error_type.subtypes.push(variant.ident.to_string());
+                match &variant.fields {
+                    Fields::Unnamed(FieldsUnnamed { ref unnamed, .. }) => {
+                        // Subtype
+                        if unnamed.iter().count() > 1 {
+                            panic!(
+                                "Error types doesn't support tuple variants with multiple fields"
+                            );
+                        }
+                    }
+                    Fields::Named(FieldsNamed { ref named, .. }) => {
+                        // If variant is Enum with a named fields - create a new type for each variant with named props
+                        let mut error_type = ErrorType::default();
+                        error_type.name = variant.ident.to_string();
+                        for field in named {
+                            error_type.props.insert(
+                                field
+                                    .ident
+                                    .as_ref()
+                                    .expect("named fields must have ident")
+                                    .to_string(),
+                                "".to_owned(),
+                            );
+                        }
+                        direct_error_types.push(error_type);
+                    }
+                    Fields::Unit => {
+                        direct_error_types.push(ErrorType {
+                            name: variant.ident.to_string(),
+                            ..Default::default()
+                        });
+                    }
+                }
+            }
+            for e in direct_error_types {
+                let mut error_type = error_type_name(schema, e.name.clone());
+                error_type.name = e.name;
+                error_type.props = e.props;
+            }
+        }
+        Data::Struct(DataStruct { ref fields, .. }) => {
+            let error_type = error_type_name(schema, name);
+            match fields {
+                Fields::Named(FieldsNamed { ref named, .. }) => {
+                    for field in named {
+                        let field_name =
+                            field.ident.as_ref().expect("named fields must have ident").to_string();
+                        if field_name == "kind" {
+                            continue;
+                        }
+                        error_type.props.insert(field_name, "".to_owned()); // TODO: add prop type
+                    }
+                }
+                _ => {
+                    panic!("RpcError supports structs with the named fields only");
+                }
+            }
+        }
+        Data::Union(_) => {
+            panic!("Unions are not supported");
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use quote::quote;
+    #[test]
+    fn should_merge_kind() {
+        let mut schema = BTreeMap::default();
+        let error_type = syn::parse2(quote! {
+            pub struct ActionError {
+                pub index: Option<u64>,
+                pub kind: ActionErrorKind,
+            }
+        })
+        .unwrap();
+        parse_error_type(&mut schema, &error_type);
+        let expected: BTreeMap<String, ErrorType> = serde_json::from_str(
+            r#"
+        {
+            "ActionError": {
+                "name": "ActionError",
+                "subtypes": [],
+                "props": {
+                "index": ""
+                }
+            }
+          }
+        "#,
+        )
+        .unwrap();
+        assert_eq!(
+            serde_json::to_string(&expected).unwrap(),
+            serde_json::to_string(&schema).unwrap()
+        );
+        let error_type_kind: DeriveInput = syn::parse2(quote! {
+            pub enum ActionErrorKind {
+                AccountAlreadyExists { account_id: String },
+            }
+        })
+        .unwrap();
+        let expected: BTreeMap<String, ErrorType> = serde_json::from_str(
+            r#"
+        {
+            "ActionError": {
+                "name": "ActionError",
+                "subtypes": ["AccountAlreadyExists"],
+                "props": {
+                    "index": ""
+                }
+            },
+            "AccountAlreadyExists": {
+                "name": "AccountAlreadyExists",
+                "subtypes": [],
+                "props": {
+                    "account_id": ""
+                }
+            }
+          }
+        "#,
+        )
+        .unwrap();
+        parse_error_type(&mut schema, &error_type_kind);
+        assert_eq!(
+            serde_json::to_string(&expected).unwrap(),
+            serde_json::to_string(&schema).unwrap()
+        );
+    }
+
+    #[test]
+    fn complex() {
+        let mut schema = BTreeMap::default();
+        parse_error_type(
+            &mut schema,
+            &syn::parse2(quote! {
+                pub enum TxExecutionError {
+                    ActionError(ActionError),
+                    InvalidTxError(InvalidTxError),
+                }
+            })
+            .unwrap(),
+        );
+        parse_error_type(
+            &mut schema,
+            &syn::parse2(quote! {
+                pub enum InvalidTxError {
+                    InvalidAccessKeyError(InvalidAccessKeyError),
+                    InvalidSignerId { signer_id: AccountId },
+                }
+            })
+            .unwrap(),
+        );
+        parse_error_type(
+            &mut schema,
+            &syn::parse2(quote! {
+                pub enum InvalidAccessKeyError {
+                    /// The access key identified by the `public_key` doesn't exist for the account
+                    AccessKeyNotFound { account_id: AccountId, public_key: PublicKey },
+                }
+            })
+            .unwrap(),
+        );
+        parse_error_type(
+            &mut schema,
+            &syn::parse2(quote! {
+                pub struct ActionError {
+                    pub index: Option<u64>,
+                    pub kind: ActionErrorKind,
+                }
+            })
+            .unwrap(),
+        );
+        parse_error_type(
+            &mut schema,
+            &syn::parse2(quote! {
+                pub enum ActionErrorKind {
+                    AccountAlreadyExists { account_id: String },
+                }
+            })
+            .unwrap(),
+        );
+        let expected: BTreeMap<String, ErrorType> = serde_json::from_str(
+            r#"
+            {
+                "AccessKeyNotFound": {
+                  "name": "AccessKeyNotFound",
+                  "subtypes": [],
+                  "props": {
+                    "account_id": "",
+                    "public_key": ""
+                  }
+                },
+                "AccountAlreadyExists": {
+                  "name": "AccountAlreadyExists",
+                  "subtypes": [],
+                  "props": {
+                    "account_id": ""
+                  }
+                },
+                "ActionError": {
+                  "name": "ActionError",
+                  "subtypes": [
+                    "AccountAlreadyExists"
+                  ],
+                  "props": {
+                    "index": ""
+                  }
+                },
+                "InvalidAccessKeyError": {
+                  "name": "InvalidAccessKeyError",
+                  "subtypes": [
+                    "AccessKeyNotFound"
+                  ],
+                  "props": {}
+                },
+                "InvalidSignerId": {
+                  "name": "InvalidSignerId",
+                  "subtypes": [],
+                  "props": {
+                    "signer_id": ""
+                  }
+                },
+                "InvalidTxError": {
+                  "name": "InvalidTxError",
+                  "subtypes": [
+                    "InvalidAccessKeyError",
+                    "InvalidSignerId"
+                  ],
+                  "props": {}
+                },
+                "TxExecutionError": {
+                  "name": "TxExecutionError",
+                  "subtypes": [
+                    "ActionError",
+                    "InvalidTxError"
+                  ],
+                  "props": {}
+                }
+              }"#,
+        )
+        .unwrap();
+        assert_eq!(
+            serde_json::to_string(&expected).unwrap(),
+            serde_json::to_string(&schema).unwrap()
+        );
+    }
+    #[test]
+    #[should_panic]
+    fn should_not_accept_tuples() {
+        let mut schema = BTreeMap::default();
+        parse_error_type(
+            &mut schema,
+            &syn::parse2(quote! {
+                pub enum ErrorWithATupleVariant {
+                    Var(One, Two)
+                }
+            })
+            .unwrap(),
+        );
+    }
+}
diff --git a/tools/rpctypegen/macro/Cargo.toml b/tools/rpctypegen/macro/Cargo.toml
new file mode 100644
index 00000000000..0d382a8b203
--- /dev/null
+++ b/tools/rpctypegen/macro/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "near-rpc-error-macro"
+version = "0.1.0"
+authors = ["Near Inc <hello@nearprotocol.com>"]
+edition = "2018"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = {version = "1.0", features = ["preserve_order"]}
+syn = { version = "1.0", features = ["full", "extra-traits"]}
+quote = "1.0"
+proc-macro2 = "1.0"
+near-rpc-error-core = { path = "../core/." }
+
+[features]
+test = []
+dump_errors_schema = ["near-rpc-error-core/dump_errors_schema"]
diff --git a/tools/rpctypegen/macro/src/lib.rs b/tools/rpctypegen/macro/src/lib.rs
new file mode 100644
index 00000000000..e44f9f9782d
--- /dev/null
+++ b/tools/rpctypegen/macro/src/lib.rs
@@ -0,0 +1,64 @@
+extern crate proc_macro;
+extern crate proc_macro2;
+
+use near_rpc_error_core::{parse_error_type, ErrorType};
+use proc_macro::TokenStream;
+use serde::{Deserialize, Serialize};
+#[cfg(feature = "dump_errors_schema")]
+use serde_json::Value;
+use std::cell::RefCell;
+use std::collections::BTreeMap;
+use syn::{parse_macro_input, DeriveInput};
+
+thread_local!(static SCHEMA: RefCell<Schema> = RefCell::new(Schema::default()));
+
+#[derive(Default, Debug, Deserialize, Serialize)]
+struct Schema {
+    pub schema: BTreeMap<String, ErrorType>,
+}
+
+#[cfg(feature = "dump_errors_schema")]
+fn merge(a: &mut Value, b: &Value) {
+    match (a, b) {
+        (&mut Value::Object(ref mut a), &Value::Object(ref b)) => {
+            for (k, v) in b {
+                merge(a.entry(k.clone()).or_insert(Value::Null), v);
+            }
+        }
+        (a, b) => {
+            *a = b.clone();
+        }
+    }
+}
+
+#[cfg(feature = "dump_errors_schema")]
+impl Drop for Schema {
+    fn drop(&mut self) {
+        // std::env::var("CARGO_TARGET_DIR") doesn't exists
+        let filename = "./target/rpc_errors_schema.json";
+        let schema_json = serde_json::to_value(self).expect("Schema serialize failed");
+        let new_schema_json = if let Ok(data) = std::fs::read(filename) {
+            // merge to the existing file
+            let mut existing_schema = serde_json::from_slice::<Value>(&data)
+                .expect("cannot deserialize target/existing_schema.json");
+            merge(&mut existing_schema, &schema_json);
+            existing_schema
+        } else {
+            schema_json
+        };
+        let new_schema_json_string = serde_json::to_string_pretty(&new_schema_json)
+            .expect("error schema serialization failed");
+        std::fs::write(filename, new_schema_json_string)
+            .expect("Unable to save the errors schema file");
+    }
+}
+
+#[proc_macro_derive(RpcError)]
+pub fn rpc_error(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+
+    SCHEMA.with(|s| {
+        parse_error_type(&mut s.borrow_mut().schema, &input);
+    });
+    TokenStream::new()
+}