diff --git a/crypto/openssl/openssl.c b/crypto/openssl/openssl.c
index 17320ae9324ca599f0e50934db1b52ae3a8dfe6c..5e6a8493074a8eea23b44aaeadfd3ccd5ef8f721 100644
--- a/crypto/openssl/openssl.c
+++ b/crypto/openssl/openssl.c
@@ -381,11 +381,13 @@ int ngtcp2_crypto_set_remote_transport_params(ngtcp2_conn *conn, void *tls,
 
   rv = ngtcp2_decode_transport_params(&params, exttype, tp, tplen);
   if (rv != 0) {
+    ngtcp2_conn_set_tls_error(conn, rv);
     return -1;
   }
 
   rv = ngtcp2_conn_set_remote_transport_params(conn, &params);
   if (rv != 0) {
+    ngtcp2_conn_set_tls_error(conn, rv);
     return -1;
   }
 
diff --git a/examples/client.cc b/examples/client.cc
index 84253fe187e4cc5fbdb0e0a44a4240d35f32748e..d25c54dbee6eebf0ceb997cb5c5a234c4ddb1043 100644
--- a/examples/client.cc
+++ b/examples/client.cc
@@ -475,6 +475,9 @@ int recv_crypto_data(ngtcp2_conn *conn, ngtcp2_crypto_level crypto_level,
   auto c = static_cast<Client *>(user_data);
 
   if (c->recv_crypto_data(crypto_level, data, datalen) != 0) {
+    if (auto err = ngtcp2_conn_get_tls_error(conn); err) {
+      return err;
+    }
     return NGTCP2_ERR_CRYPTO;
   }
 
diff --git a/examples/server.cc b/examples/server.cc
index 427a3d29af5719dc97ee577373b19edfffec0a9b..8797fe1c7c19840a83d724cb92cc95e6bdda5259 100644
--- a/examples/server.cc
+++ b/examples/server.cc
@@ -772,6 +772,9 @@ int recv_crypto_data(ngtcp2_conn *conn, ngtcp2_crypto_level crypto_level,
   auto h = static_cast<Handler *>(user_data);
 
   if (h->recv_crypto_data(crypto_level, data, datalen) != 0) {
+    if (auto err = ngtcp2_conn_get_tls_error(conn); err) {
+      return err;
+    }
     return NGTCP2_ERR_CRYPTO;
   }
 
diff --git a/lib/includes/ngtcp2/ngtcp2.h b/lib/includes/ngtcp2/ngtcp2.h
index 917cbb6f227404c138bc7b4a5c02a32016ccc6bb..936b876ef685c3f825b5b1c58e1ec100db1735a7 100644
--- a/lib/includes/ngtcp2/ngtcp2.h
+++ b/lib/includes/ngtcp2/ngtcp2.h
@@ -1932,6 +1932,27 @@ NGTCP2_EXTERN int ngtcp2_conn_install_key(
 NGTCP2_EXTERN int ngtcp2_conn_initiate_key_update(ngtcp2_conn *conn,
                                                   ngtcp2_tstamp ts);
 
+/**
+ * @function
+ *
+ * `ngtcp2_conn_set_tls_error` sets the TLS related error in |conn|.
+ * In general, error code should be propagated via return value, but
+ * sometimes ngtcp2 API is called inside callback function of TLS
+ * stack and it does not allow to return ngtcp2 error code directly.
+ * In this case, implementation can set the error code (e.g.,
+ * NGTCP2_ERR_MALFORMED_TRANSPORT_PARAM) using this function.
+ */
+NGTCP2_EXTERN void ngtcp2_conn_set_tls_error(ngtcp2_conn *conn, int liberr);
+
+/**
+ * @function
+ *
+ * `ngtcp2_conn_get_tls_error` returns the value set by
+ * `ngtcp2_conn_set_tls_error`.  If no value is set, this function
+ * returns 0.
+ */
+NGTCP2_EXTERN int ngtcp2_conn_get_tls_error(ngtcp2_conn *conn);
+
 /**
  * @function
  *
diff --git a/lib/ngtcp2_conn.c b/lib/ngtcp2_conn.c
index 4daf7914a87b5f4d996054a86b4aa841205e2ebd..bda3fde125d44fae452126ec235ce9375ed72080 100644
--- a/lib/ngtcp2_conn.c
+++ b/lib/ngtcp2_conn.c
@@ -9261,6 +9261,14 @@ void ngtcp2_conn_get_connection_close_error_code(
   *ccec = conn->rx.ccec;
 }
 
+void ngtcp2_conn_set_tls_error(ngtcp2_conn *conn, int liberr) {
+  conn->crypto.tls_error = liberr;
+}
+
+int ngtcp2_conn_get_tls_error(ngtcp2_conn *conn) {
+  return conn->crypto.tls_error;
+}
+
 void ngtcp2_path_challenge_entry_init(ngtcp2_path_challenge_entry *pcent,
                                       const uint8_t *data) {
   memcpy(pcent->data, data, sizeof(pcent->data));
diff --git a/lib/ngtcp2_conn.h b/lib/ngtcp2_conn.h
index ba3a61a8eb799f2b68012320ac1bf7c2e4cff823..3b17a3d230ada7f310ec6ecf486c206e9d41f5d8 100644
--- a/lib/ngtcp2_conn.h
+++ b/lib/ngtcp2_conn.h
@@ -422,6 +422,8 @@ struct ngtcp2_conn {
     ngtcp2_vec decrypt_buf;
     /* retry_aead is AEAD to verify Retry packet integrity. */
     ngtcp2_crypto_aead retry_aead;
+    /* tls_error is TLS related error. */
+    int tls_error;
   } crypto;
 
   /* pkt contains the packet intermediate construction data to support
diff --git a/lib/ngtcp2_err.c b/lib/ngtcp2_err.c
index 10b6ef81d5e15622e8066cc9074877a173fbe20e..ae2edb35ee428fde5b3e4833d7403091748602a9 100644
--- a/lib/ngtcp2_err.c
+++ b/lib/ngtcp2_err.c
@@ -122,6 +122,7 @@ uint64_t ngtcp2_err_infer_quic_transport_error_code(int liberr) {
     return NGTCP2_FINAL_SIZE_ERROR;
   case NGTCP2_ERR_REQUIRED_TRANSPORT_PARAM:
   case NGTCP2_ERR_MALFORMED_TRANSPORT_PARAM:
+  case NGTCP2_ERR_TRANSPORT_PARAM:
     return NGTCP2_TRANSPORT_PARAMETER_ERROR;
   case NGTCP2_ERR_INVALID_ARGUMENT:
     return NGTCP2_INTERNAL_ERROR;