diff --git a/bridges/bin/runtime-common/src/messages.rs b/bridges/bin/runtime-common/src/messages.rs
index ceedee12afabfa5be73eaa9fde5ac827fba46e7e..4eb44ea85ed456d89dcdde82bd7a36b8c8c4b79c 100644
--- a/bridges/bin/runtime-common/src/messages.rs
+++ b/bridges/bin/runtime-common/src/messages.rs
@@ -100,6 +100,29 @@ pub type OriginOf<C> = <C as ThisChainWithMessages>::RuntimeOrigin;
 /// Type of call that is used on this chain.
 pub type CallOf<C> = <C as ThisChainWithMessages>::RuntimeCall;
 
+/// Error that happens during message verification.
+#[derive(Debug, PartialEq, Eq)]
+pub enum Error {
+	/// The message proof is empty.
+	EmptyMessageProof,
+	/// Error returned by the bridged header chain.
+	HeaderChain(HeaderChainError),
+	/// Error returned while reading/decoding inbound lane data from the storage proof.
+	InboundLaneStorage(StorageProofError),
+	/// The declared message weight is incorrect.
+	InvalidMessageWeight,
+	/// Declared messages count doesn't match actual value.
+	MessagesCountMismatch,
+	/// Error returned while reading/decoding message data from the storage proof.
+	MessageStorage(StorageProofError),
+	/// The message is too large.
+	MessageTooLarge,
+	/// Error returned while reading/decoding outbound lane data from the storage proof.
+	OutboundLaneStorage(StorageProofError),
+	/// Storage proof related error.
+	StorageProof(StorageProofError),
+}
+
 /// Sub-module that is declaring types required for processing This -> Bridged chain messages.
 pub mod source {
 	use super::*;
@@ -169,8 +192,6 @@ pub mod source {
 	/// The error message returned from `LaneMessageVerifier` when too many pending messages at the
 	/// lane.
 	pub const TOO_MANY_PENDING_MESSAGES: &str = "Too many pending messages at the lane.";
-	/// The error message returned from `LaneMessageVerifier` when call origin is mismatch.
-	pub const BAD_ORIGIN: &str = "Unable to match the source origin to expected target origin.";
 
 	impl<B> LaneMessageVerifier<OriginOf<ThisChain<B>>, FromThisChainMessagePayload>
 		for FromThisChainMessageVerifier<B>
@@ -220,7 +241,7 @@ pub mod source {
 	impl<B: MessageBridge> TargetHeaderChain<FromThisChainMessagePayload, AccountIdOf<ThisChain<B>>>
 		for TargetHeaderChainAdapter<B>
 	{
-		type Error = &'static str;
+		type Error = Error;
 		type MessagesDeliveryProof = FromBridgedChainMessagesDeliveryProof<HashOf<BridgedChain<B>>>;
 
 		fn verify_message(payload: &FromThisChainMessagePayload) -> Result<(), Self::Error> {
@@ -241,9 +262,9 @@ pub mod source {
 	/// check) that would reject message (see `FromThisChainMessageVerifier`).
 	pub fn verify_chain_message<B: MessageBridge>(
 		payload: &FromThisChainMessagePayload,
-	) -> Result<(), &'static str> {
+	) -> Result<(), Error> {
 		if !BridgedChain::<B>::verify_dispatch_weight(payload) {
-			return Err("Incorrect message weight declared")
+			return Err(Error::InvalidMessageWeight)
 		}
 
 		// The maximal size of extrinsic at Substrate-based chain depends on the
@@ -257,7 +278,7 @@ pub mod source {
 		// transaction also contains signatures and signed extensions. Because of this, we reserve
 		// 1/3 of the the maximal extrinsic weight for this data.
 		if payload.len() > maximal_message_size::<B>() as usize {
-			return Err("The message is too large to be sent over the lane")
+			return Err(Error::MessageTooLarge)
 		}
 
 		Ok(())
@@ -269,7 +290,7 @@ pub mod source {
 	/// parachains, please use the `verify_messages_delivery_proof_from_parachain`.
 	pub fn verify_messages_delivery_proof<B: MessageBridge>(
 		proof: FromBridgedChainMessagesDeliveryProof<HashOf<BridgedChain<B>>>,
-	) -> Result<ParsedMessagesDeliveryProofFromBridgedChain<B>, &'static str> {
+	) -> Result<ParsedMessagesDeliveryProofFromBridgedChain<B>, Error> {
 		let FromBridgedChainMessagesDeliveryProof { bridged_header_hash, storage_proof, lane } =
 			proof;
 		B::BridgedHeaderChain::parse_finalized_storage_proof(
@@ -283,22 +304,17 @@ pub mod source {
 						B::BRIDGED_MESSAGES_PALLET_NAME,
 						&lane,
 					);
-				let raw_inbound_lane_data = storage
-					.read_value(storage_inbound_lane_data_key.0.as_ref())
-					.map_err(|_| "Failed to read inbound lane state from storage proof")?
-					.ok_or("Inbound lane state is missing from the messages proof")?;
-				let inbound_lane_data = InboundLaneData::decode(&mut &raw_inbound_lane_data[..])
-					.map_err(|_| "Failed to decode inbound lane state from the proof")?;
+				let inbound_lane_data = storage
+					.read_and_decode_mandatory_value(storage_inbound_lane_data_key.0.as_ref())
+					.map_err(Error::InboundLaneStorage)?;
 
 				// check that the storage proof doesn't have any untouched trie nodes
-				storage
-					.ensure_no_unused_nodes()
-					.map_err(|_| "Messages delivery proof has unused trie nodes")?;
+				storage.ensure_no_unused_nodes().map_err(Error::StorageProof)?;
 
 				Ok((lane, inbound_lane_data))
 			},
 		)
-		.map_err(<&'static str>::from)?
+		.map_err(Error::HeaderChain)?
 	}
 
 	/// XCM bridge.
@@ -580,14 +596,14 @@ pub mod target {
 	pub struct SourceHeaderChainAdapter<B>(PhantomData<B>);
 
 	impl<B: MessageBridge> SourceHeaderChain for SourceHeaderChainAdapter<B> {
-		type Error = &'static str;
+		type Error = Error;
 		type MessagesProof = FromBridgedChainMessagesProof<HashOf<BridgedChain<B>>>;
 
 		fn verify_messages_proof(
 			proof: Self::MessagesProof,
 			messages_count: u32,
 		) -> Result<ProvedMessages<Message>, Self::Error> {
-			verify_messages_proof::<B>(proof, messages_count).map_err(Into::into)
+			verify_messages_proof::<B>(proof, messages_count)
 		}
 	}
 
@@ -602,7 +618,7 @@ pub mod target {
 	pub fn verify_messages_proof<B: MessageBridge>(
 		proof: FromBridgedChainMessagesProof<HashOf<BridgedChain<B>>>,
 		messages_count: u32,
-	) -> Result<ProvedMessages<Message>, MessageProofError> {
+	) -> Result<ProvedMessages<Message>, Error> {
 		let FromBridgedChainMessagesProof {
 			bridged_header_hash,
 			storage_proof,
@@ -625,7 +641,7 @@ pub mod target {
 						// (this bounds maximal capacity of messages vec below)
 						let messages_in_the_proof = nonces_difference.saturating_add(1);
 						if messages_in_the_proof != MessageNonce::from(messages_count) {
-							return Err(MessageProofError::MessagesCountMismatch)
+							return Err(Error::MessagesCountMismatch)
 						}
 
 						messages_in_the_proof
@@ -640,37 +656,26 @@ pub mod target {
 				let mut messages = Vec::with_capacity(messages_in_the_proof as _);
 				for nonce in nonces_start..=nonces_end {
 					let message_key = MessageKey { lane_id: lane, nonce };
-					let raw_message_data = parser
-						.read_raw_message(&message_key)
-						.ok_or(MessageProofError::MissingRequiredMessage)?;
-					let payload = MessagePayload::decode(&mut &raw_message_data[..])
-						.map_err(|_| MessageProofError::FailedToDecodeMessage)?;
-					messages.push(Message { key: message_key, payload });
+					let message_payload = parser.read_and_decode_message_payload(&message_key)?;
+					messages.push(Message { key: message_key, payload: message_payload });
 				}
 
 				// Now let's check if proof contains outbound lane state proof. It is optional, so
 				// we simply ignore `read_value` errors and missing value.
-				let mut proved_lane_messages = ProvedLaneMessages { lane_state: None, messages };
-				let raw_outbound_lane_data = parser.read_raw_outbound_lane_data(&lane);
-				if let Some(raw_outbound_lane_data) = raw_outbound_lane_data {
-					proved_lane_messages.lane_state = Some(
-						OutboundLaneData::decode(&mut &raw_outbound_lane_data[..])
-							.map_err(|_| MessageProofError::FailedToDecodeOutboundLaneState)?,
-					);
-				}
+				let proved_lane_messages = ProvedLaneMessages {
+					lane_state: parser.read_and_decode_outbound_lane_data(&lane)?,
+					messages,
+				};
 
 				// Now we may actually check if the proof is empty or not.
 				if proved_lane_messages.lane_state.is_none() &&
 					proved_lane_messages.messages.is_empty()
 				{
-					return Err(MessageProofError::Empty)
+					return Err(Error::EmptyMessageProof)
 				}
 
 				// check that the storage proof doesn't have any untouched trie nodes
-				parser
-					.storage
-					.ensure_no_unused_nodes()
-					.map_err(MessageProofError::StorageProof)?;
+				parser.storage.ensure_no_unused_nodes().map_err(Error::StorageProof)?;
 
 				// We only support single lane messages in this generated_schema
 				let mut proved_messages = ProvedMessages::new();
@@ -679,43 +684,7 @@ pub mod target {
 				Ok(proved_messages)
 			},
 		)
-		.map_err(MessageProofError::HeaderChain)?
-	}
-
-	/// Error that happens during message proof verification.
-	#[derive(Debug, PartialEq, Eq)]
-	pub enum MessageProofError {
-		/// Error returned by the bridged header chain.
-		HeaderChain(HeaderChainError),
-		/// The message proof is empty.
-		Empty,
-		/// Declared messages count doesn't match actual value.
-		MessagesCountMismatch,
-		/// Message is missing from the proof.
-		MissingRequiredMessage,
-		/// Failed to decode message from the proof.
-		FailedToDecodeMessage,
-		/// Failed to decode outbound lane data from the proof.
-		FailedToDecodeOutboundLaneState,
-		/// Storage proof related error.
-		StorageProof(StorageProofError),
-	}
-
-	impl From<MessageProofError> for &'static str {
-		fn from(err: MessageProofError) -> &'static str {
-			match err {
-				MessageProofError::HeaderChain(err) => err.into(),
-				MessageProofError::Empty => "Messages proof is empty",
-				MessageProofError::MessagesCountMismatch =>
-					"Declared messages count doesn't match actual value",
-				MessageProofError::MissingRequiredMessage => "Message is missing from the proof",
-				MessageProofError::FailedToDecodeMessage =>
-					"Failed to decode message from the proof",
-				MessageProofError::FailedToDecodeOutboundLaneState =>
-					"Failed to decode outbound lane data from the proof",
-				MessageProofError::StorageProof(_) => "Invalid storage proof",
-			}
-		}
+		.map_err(Error::HeaderChain)?
 	}
 
 	struct StorageProofCheckerAdapter<H: Hasher, B> {
@@ -724,21 +693,32 @@ pub mod target {
 	}
 
 	impl<H: Hasher, B: MessageBridge> StorageProofCheckerAdapter<H, B> {
-		fn read_raw_outbound_lane_data(&mut self, lane_id: &LaneId) -> Option<Vec<u8>> {
+		fn read_and_decode_outbound_lane_data(
+			&mut self,
+			lane_id: &LaneId,
+		) -> Result<Option<OutboundLaneData>, Error> {
 			let storage_outbound_lane_data_key = bp_messages::storage_keys::outbound_lane_data_key(
 				B::BRIDGED_MESSAGES_PALLET_NAME,
 				lane_id,
 			);
-			self.storage.read_value(storage_outbound_lane_data_key.0.as_ref()).ok()?
+
+			self.storage
+				.read_and_decode_opt_value(storage_outbound_lane_data_key.0.as_ref())
+				.map_err(Error::OutboundLaneStorage)
 		}
 
-		fn read_raw_message(&mut self, message_key: &MessageKey) -> Option<Vec<u8>> {
+		fn read_and_decode_message_payload(
+			&mut self,
+			message_key: &MessageKey,
+		) -> Result<MessagePayload, Error> {
 			let storage_message_key = bp_messages::storage_keys::message_key(
 				B::BRIDGED_MESSAGES_PALLET_NAME,
 				&message_key.lane_id,
 				message_key.nonce,
 			);
-			self.storage.read_value(storage_message_key.0.as_ref()).ok()?
+			self.storage
+				.read_and_decode_mandatory_value(storage_message_key.0.as_ref())
+				.map_err(Error::MessageStorage)
 		}
 	}
 }
@@ -896,7 +876,7 @@ mod tests {
 			using_messages_proof(10, None, encode_all_messages, encode_lane_data, |proof| {
 				target::verify_messages_proof::<OnThisChainBridge>(proof, 5)
 			}),
-			Err(target::MessageProofError::MessagesCountMismatch),
+			Err(Error::MessagesCountMismatch),
 		);
 	}
 
@@ -906,7 +886,7 @@ mod tests {
 			using_messages_proof(10, None, encode_all_messages, encode_lane_data, |proof| {
 				target::verify_messages_proof::<OnThisChainBridge>(proof, 15)
 			}),
-			Err(target::MessageProofError::MessagesCountMismatch),
+			Err(Error::MessagesCountMismatch),
 		);
 	}
 
@@ -919,7 +899,7 @@ mod tests {
 				pallet_bridge_grandpa::ImportedHeaders::<TestRuntime>::remove(bridged_header_hash);
 				target::verify_messages_proof::<OnThisChainBridge>(proof, 10)
 			}),
-			Err(target::MessageProofError::HeaderChain(HeaderChainError::UnknownHeader)),
+			Err(Error::HeaderChain(HeaderChainError::UnknownHeader)),
 		);
 	}
 
@@ -942,7 +922,7 @@ mod tests {
 				);
 				target::verify_messages_proof::<OnThisChainBridge>(proof, 10)
 			}),
-			Err(target::MessageProofError::HeaderChain(HeaderChainError::StorageProof(
+			Err(Error::HeaderChain(HeaderChainError::StorageProof(
 				StorageProofError::StorageRootMismatch
 			))),
 		);
@@ -957,7 +937,7 @@ mod tests {
 				proof.storage_proof.push(node);
 				target::verify_messages_proof::<OnThisChainBridge>(proof, 10)
 			},),
-			Err(target::MessageProofError::HeaderChain(HeaderChainError::StorageProof(
+			Err(Error::HeaderChain(HeaderChainError::StorageProof(
 				StorageProofError::DuplicateNodesInProof
 			))),
 		);
@@ -970,13 +950,13 @@ mod tests {
 				proof.storage_proof.push(vec![42]);
 				target::verify_messages_proof::<OnThisChainBridge>(proof, 10)
 			},),
-			Err(target::MessageProofError::StorageProof(StorageProofError::UnusedNodesInTheProof)),
+			Err(Error::StorageProof(StorageProofError::UnusedNodesInTheProof)),
 		);
 	}
 
 	#[test]
 	fn message_proof_is_rejected_if_required_message_is_missing() {
-		assert_eq!(
+		matches!(
 			using_messages_proof(
 				10,
 				None,
@@ -984,13 +964,13 @@ mod tests {
 				encode_lane_data,
 				|proof| target::verify_messages_proof::<OnThisChainBridge>(proof, 10)
 			),
-			Err(target::MessageProofError::MissingRequiredMessage),
+			Err(Error::MessageStorage(StorageProofError::StorageValueEmpty)),
 		);
 	}
 
 	#[test]
 	fn message_proof_is_rejected_if_message_decode_fails() {
-		assert_eq!(
+		matches!(
 			using_messages_proof(
 				10,
 				None,
@@ -1004,13 +984,13 @@ mod tests {
 				encode_lane_data,
 				|proof| target::verify_messages_proof::<OnThisChainBridge>(proof, 10),
 			),
-			Err(target::MessageProofError::FailedToDecodeMessage),
+			Err(Error::MessageStorage(StorageProofError::StorageValueDecodeFailed(_))),
 		);
 	}
 
 	#[test]
 	fn message_proof_is_rejected_if_outbound_lane_state_decode_fails() {
-		assert_eq!(
+		matches!(
 			using_messages_proof(
 				10,
 				Some(OutboundLaneData {
@@ -1026,7 +1006,7 @@ mod tests {
 				},
 				|proof| target::verify_messages_proof::<OnThisChainBridge>(proof, 10),
 			),
-			Err(target::MessageProofError::FailedToDecodeOutboundLaneState),
+			Err(Error::OutboundLaneStorage(StorageProofError::StorageValueDecodeFailed(_))),
 		);
 	}
 
@@ -1036,7 +1016,7 @@ mod tests {
 			using_messages_proof(0, None, encode_all_messages, encode_lane_data, |proof| {
 				target::verify_messages_proof::<OnThisChainBridge>(proof, 0)
 			},),
-			Err(target::MessageProofError::Empty),
+			Err(Error::EmptyMessageProof),
 		);
 	}
 
@@ -1110,7 +1090,7 @@ mod tests {
 				proof.nonces_end = u64::MAX;
 				target::verify_messages_proof::<OnThisChainBridge>(proof, u32::MAX)
 			},),
-			Err(target::MessageProofError::MessagesCountMismatch),
+			Err(Error::MessagesCountMismatch),
 		);
 	}
 }
diff --git a/bridges/modules/parachains/src/lib.rs b/bridges/modules/parachains/src/lib.rs
index b9bbaac7ca3d401e990d51fbb33ae30778e8d3ac..98556f737bac35f6845bb9e6cfe282cb48e9c0b6 100644
--- a/bridges/modules/parachains/src/lib.rs
+++ b/bridges/modules/parachains/src/lib.rs
@@ -26,7 +26,7 @@
 pub use weights::WeightInfo;
 pub use weights_ext::WeightInfoExt;
 
-use bp_header_chain::HeaderChain;
+use bp_header_chain::{HeaderChain, HeaderChainError};
 use bp_parachains::{parachain_head_storage_key_at_source, ParaInfo, ParaStoredHeaderData};
 use bp_polkadot_core::parachains::{ParaHash, ParaHead, ParaHeadsProof, ParaId};
 use bp_runtime::{Chain, HashOf, HeaderId, HeaderIdOf, Parachain, StorageProofError};
@@ -125,8 +125,8 @@ pub mod pallet {
 		UnknownRelayChainBlock,
 		/// The number of stored relay block is different from what the relayer has provided.
 		InvalidRelayChainBlockNumber,
-		/// Invalid storage proof has been passed.
-		InvalidStorageProof,
+		/// Error generated by a method defined in `bp-header-chain`.
+		HeaderChain(HeaderChainError),
 		/// Given parachain head is unknown.
 		UnknownParaHead,
 		/// The storage proof doesn't contains storage root. So it is invalid for given header.
@@ -430,10 +430,10 @@ pub mod pallet {
 					storage.ensure_no_unused_nodes()
 				},
 			)
-			.and_then(|r| r.map_err(bp_header_chain::HeaderChainError::StorageProof))
+			.and_then(|r| r.map_err(HeaderChainError::StorageProof))
 			.map_err(|e| {
 				log::trace!(target: LOG_TARGET, "Parachain heads storage proof is invalid: {:?}", e);
-				Error::<T, I>::InvalidStorageProof
+				Error::<T, I>::HeaderChain(e)
 			})?;
 
 			Ok(PostDispatchInfo { actual_weight: Some(actual_weight), pays_fee: Pays::Yes })
@@ -1379,7 +1379,9 @@ mod tests {
 			// try to import head#5 of parachain#1 at relay chain block #0
 			assert_noop!(
 				import_parachain_1_head(0, Default::default(), parachains, proof),
-				Error::<TestRuntime>::InvalidStorageProof
+				Error::<TestRuntime>::HeaderChain(HeaderChainError::StorageProof(
+					StorageProofError::StorageRootMismatch
+				))
 			);
 		});
 	}
diff --git a/bridges/primitives/header-chain/src/lib.rs b/bridges/primitives/header-chain/src/lib.rs
index cafb8e7a3c86f98658699717421df986b20d65e1..5d05a1b15d8fa53d5b60adc31faec1d559abe9bd 100644
--- a/bridges/primitives/header-chain/src/lib.rs
+++ b/bridges/primitives/header-chain/src/lib.rs
@@ -25,6 +25,7 @@ use bp_runtime::{
 };
 use codec::{Codec, Decode, Encode, EncodeLike, MaxEncodedLen};
 use core::{clone::Clone, cmp::Eq, default::Default, fmt::Debug};
+use frame_support::PalletError;
 use scale_info::TypeInfo;
 #[cfg(feature = "std")]
 use serde::{Deserialize, Serialize};
@@ -36,7 +37,7 @@ pub mod justification;
 pub mod storage_keys;
 
 /// Header chain error.
-#[derive(Clone, Eq, PartialEq, RuntimeDebug)]
+#[derive(Clone, Decode, Encode, Eq, PartialEq, PalletError, Debug, TypeInfo)]
 pub enum HeaderChainError {
 	/// Header with given hash is missing from the chain.
 	UnknownHeader,
@@ -44,15 +45,6 @@ pub enum HeaderChainError {
 	StorageProof(StorageProofError),
 }
 
-impl From<HeaderChainError> for &'static str {
-	fn from(err: HeaderChainError) -> &'static str {
-		match err {
-			HeaderChainError::UnknownHeader => "UnknownHeader",
-			HeaderChainError::StorageProof(e) => e.into(),
-		}
-	}
-}
-
 /// Header data that we're storing on-chain.
 ///
 /// Even though we may store full header, our applications (XCM) only use couple of header
diff --git a/bridges/primitives/messages/src/source_chain.rs b/bridges/primitives/messages/src/source_chain.rs
index d5d7549099c31598bc39490ed206ad5d0a277c6d..881bd92f5f955baff755c20f4a3184382bdab5c7 100644
--- a/bridges/primitives/messages/src/source_chain.rs
+++ b/bridges/primitives/messages/src/source_chain.rs
@@ -41,7 +41,7 @@ pub type RelayersRewards<AccountId> = BTreeMap<AccountId, MessageNonce>;
 /// type used by the source chain.
 pub trait TargetHeaderChain<Payload, AccountId> {
 	/// Error type.
-	type Error: Debug + Into<&'static str>;
+	type Error: Debug;
 
 	/// Proof that messages have been received by target chain.
 	type MessagesDeliveryProof: Parameter + Size;
diff --git a/bridges/primitives/messages/src/target_chain.rs b/bridges/primitives/messages/src/target_chain.rs
index 6604d425cfc4aa2666b9db88fd0e2043e40ba8ef..8496b90214c409bd968aa1346ca4b64984baf091 100644
--- a/bridges/primitives/messages/src/target_chain.rs
+++ b/bridges/primitives/messages/src/target_chain.rs
@@ -59,7 +59,7 @@ pub struct DispatchMessage<DispatchPayload> {
 /// that's stuck) and/or processing messages without paying fees.
 pub trait SourceHeaderChain {
 	/// Error type.
-	type Error: Debug + Into<&'static str>;
+	type Error: Debug;
 
 	/// Proof that messages are sent from source chain. This may also include proof
 	/// of corresponding outbound lane states.
diff --git a/bridges/primitives/runtime/src/lib.rs b/bridges/primitives/runtime/src/lib.rs
index 2d29b5aff0ab96b1a79bcf9211aa4d3d93a8f595..ece925d7952732d5191c66be8a2ed588621fa37f 100644
--- a/bridges/primitives/runtime/src/lib.rs
+++ b/bridges/primitives/runtime/src/lib.rs
@@ -531,6 +531,37 @@ macro_rules! generate_static_str_provider {
 	};
 }
 
+#[derive(Encode, Decode, Clone, Eq, PartialEq, PalletError, TypeInfo)]
+#[scale_info(skip_type_params(T))]
+pub struct StrippableError<T> {
+	_phantom_data: sp_std::marker::PhantomData<T>,
+	#[codec(skip)]
+	#[cfg(feature = "std")]
+	message: String,
+}
+
+impl<T: Debug> From<T> for StrippableError<T> {
+	fn from(err: T) -> Self {
+		Self {
+			_phantom_data: Default::default(),
+			#[cfg(feature = "std")]
+			message: format!("{:?}", err),
+		}
+	}
+}
+
+impl<T> Debug for StrippableError<T> {
+	#[cfg(feature = "std")]
+	fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
+		f.write_str(&self.message)
+	}
+
+	#[cfg(not(feature = "std"))]
+	fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
+		f.write_str("Stripped error")
+	}
+}
+
 #[cfg(test)]
 mod tests {
 	use super::*;
diff --git a/bridges/primitives/runtime/src/storage_proof.rs b/bridges/primitives/runtime/src/storage_proof.rs
index aa9c3088fa42f6412010f2dfcf7feca860613e2d..09641376666af9feecb3f9255dc02e6fdf2adca0 100644
--- a/bridges/primitives/runtime/src/storage_proof.rs
+++ b/bridges/primitives/runtime/src/storage_proof.rs
@@ -16,9 +16,11 @@
 
 //! Logic for checking Substrate storage proofs.
 
-use codec::Decode;
+use crate::StrippableError;
+use codec::{Decode, Encode};
+use frame_support::PalletError;
 use hash_db::{HashDB, Hasher, EMPTY_PREFIX};
-use sp_runtime::RuntimeDebug;
+use scale_info::TypeInfo;
 use sp_std::{boxed::Box, collections::btree_set::BTreeSet, vec::Vec};
 use sp_trie::{
 	read_trie_value, LayoutV1, MemoryDB, Recorder, StorageProof, Trie, TrieConfiguration,
@@ -116,14 +118,32 @@ where
 	/// read, but decoding fails, this function returns an error.
 	pub fn read_and_decode_value<T: Decode>(&mut self, key: &[u8]) -> Result<Option<T>, Error> {
 		self.read_value(key).and_then(|v| {
-			v.map(|v| T::decode(&mut &v[..]).map_err(Error::StorageValueDecodeFailed))
+			v.map(|v| T::decode(&mut &v[..]).map_err(|e| Error::StorageValueDecodeFailed(e.into())))
 				.transpose()
 		})
 	}
+
+	/// Reads and decodes a value from the available subset of storage. If the value cannot be read
+	/// due to an incomplete or otherwise invalid proof, or if the value is `None`, this function
+	/// returns an error. If value is read, but decoding fails, this function returns an error.
+	pub fn read_and_decode_mandatory_value<T: Decode>(&mut self, key: &[u8]) -> Result<T, Error> {
+		self.read_and_decode_value(key)?.ok_or(Error::StorageValueEmpty)
+	}
+
+	/// Reads and decodes a value from the available subset of storage. If the value cannot be read
+	/// due to an incomplete or otherwise invalid proof, this function returns `Ok(None)`.
+	/// If value is read, but decoding fails, this function returns an error.
+	pub fn read_and_decode_opt_value<T: Decode>(&mut self, key: &[u8]) -> Result<Option<T>, Error> {
+		match self.read_and_decode_value(key) {
+			Ok(outbound_lane_data) => Ok(outbound_lane_data),
+			Err(Error::StorageValueUnavailable) => Ok(None),
+			Err(e) => Err(e),
+		}
+	}
 }
 
 /// Storage proof related errors.
-#[derive(Clone, Eq, PartialEq, RuntimeDebug)]
+#[derive(Encode, Decode, Clone, Eq, PartialEq, PalletError, Debug, TypeInfo)]
 pub enum Error {
 	/// Duplicate trie nodes are found in the proof.
 	DuplicateNodesInProof,
@@ -133,21 +153,10 @@ pub enum Error {
 	StorageRootMismatch,
 	/// Unable to reach expected storage value using provided trie nodes.
 	StorageValueUnavailable,
+	/// The storage value is `None`.
+	StorageValueEmpty,
 	/// Failed to decode storage value.
-	StorageValueDecodeFailed(codec::Error),
-}
-
-impl From<Error> for &'static str {
-	fn from(err: Error) -> &'static str {
-		match err {
-			Error::DuplicateNodesInProof => "Storage proof contains duplicate nodes",
-			Error::UnusedNodesInTheProof => "Storage proof contains unused nodes",
-			Error::StorageRootMismatch => "Storage root is missing from the storage proof",
-			Error::StorageValueUnavailable => "Storage value is missing from the storage proof",
-			Error::StorageValueDecodeFailed(_) =>
-				"Failed to decode storage value from the storage proof",
-		}
-	}
+	StorageValueDecodeFailed(StrippableError<codec::Error>),
 }
 
 /// Return valid storage proof and state root.
@@ -155,7 +164,6 @@ impl From<Error> for &'static str {
 /// NOTE: This should only be used for **testing**.
 #[cfg(feature = "std")]
 pub fn craft_valid_storage_proof() -> (sp_core::H256, RawStorageProof) {
-	use codec::Encode;
 	use sp_state_machine::{backend::Backend, prove_read, InMemoryBackend};
 
 	let state_version = sp_runtime::StateVersion::default();