Skip to main content

binance/
error_code.rs

1//! Numeric error codes returned by the Binance API across all products
2//! (spot, margin, derivatives).
3//!
4//! The codes themselves are universal — `-1021` means "invalid timestamp"
5//! whether you got it from `/api/v3/order` or `/fapi/v1/order` — so the type
6//! lives at the crate root and is re-exported from each product module
7//! (`binance::spot::ErrorCode`, `binance::margin::ErrorCode`, …).
8//!
9//! `ErrorCode` is a transparent newtype over `i64`. Use the named constants
10//! and `is_*` predicates for common cases; fall back to [`ErrorCode::raw`]
11//! for product-specific codes (the -4xxx / -5xxx ranges on futures are not
12//! exhaustively classified here).
13
14use serde::{Deserialize, Serialize};
15
16/// Numeric Binance error code, as returned in the `code` field of an error
17/// response body.
18///
19/// Kept as a newtype around `i64` rather than a closed enum because Binance
20/// adds new codes over time and product-specific endpoints use codes outside
21/// the spot range. Use the named constants for the common ones and the
22/// `is_*` predicates for category-based handling; fall back to
23/// [`ErrorCode::raw`] for anything not classified.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
25#[serde(transparent)]
26pub struct ErrorCode(pub i64);
27
28impl ErrorCode {
29    pub const fn new(code: i64) -> Self {
30        Self(code)
31    }
32
33    /// Raw numeric code as returned by Binance.
34    pub const fn raw(self) -> i64 {
35        self.0
36    }
37}
38
39impl From<i64> for ErrorCode {
40    fn from(code: i64) -> Self {
41        Self(code)
42    }
43}
44
45impl From<ErrorCode> for i64 {
46    fn from(code: ErrorCode) -> Self {
47        code.0
48    }
49}
50
51impl std::fmt::Display for ErrorCode {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        write!(f, "{}", self.0)
54    }
55}
56
57// ===== Named constants for commonly-checked codes =====
58
59impl ErrorCode {
60    // 10xx — general server / network
61    pub const UNKNOWN: Self = Self(-1000);
62    pub const DISCONNECTED: Self = Self(-1001);
63    pub const UNAUTHORIZED: Self = Self(-1002);
64    pub const TOO_MANY_REQUESTS: Self = Self(-1003);
65    pub const UNEXPECTED_RESP: Self = Self(-1006);
66    pub const TIMEOUT: Self = Self(-1007);
67    pub const SERVER_BUSY: Self = Self(-1008);
68    pub const TOO_MANY_ORDERS: Self = Self(-1015);
69    pub const SERVICE_SHUTTING_DOWN: Self = Self(-1016);
70    pub const UNSUPPORTED_OPERATION: Self = Self(-1020);
71    pub const INVALID_TIMESTAMP: Self = Self(-1021);
72    pub const INVALID_SIGNATURE: Self = Self(-1022);
73    pub const TOO_MANY_CONNECTIONS: Self = Self(-1034);
74
75    // 11xx — request validation
76    pub const ILLEGAL_CHARS: Self = Self(-1100);
77    pub const TOO_MANY_PARAMETERS: Self = Self(-1101);
78    pub const MANDATORY_PARAM_EMPTY_OR_MALFORMED: Self = Self(-1102);
79    pub const UNKNOWN_PARAM: Self = Self(-1103);
80    pub const BAD_SYMBOL: Self = Self(-1121);
81    pub const INVALID_LISTEN_KEY: Self = Self(-1125);
82    pub const TOO_MANY_MESSAGES: Self = Self(-1181);
83    pub const TOO_MANY_SUBSCRIPTIONS: Self = Self(-1191);
84
85    // 20xx — trading / matching engine
86    pub const NEW_ORDER_REJECTED: Self = Self(-2010);
87    pub const CANCEL_REJECTED: Self = Self(-2011);
88    pub const NO_SUCH_ORDER: Self = Self(-2013);
89    pub const BAD_API_KEY_FMT: Self = Self(-2014);
90    pub const REJECTED_MBX_KEY: Self = Self(-2015);
91    pub const NO_TRADING_WINDOW: Self = Self(-2016);
92    pub const ORDER_AMEND_REJECTED: Self = Self(-2038);
93    pub const CLIENT_ORDER_ID_INVALID: Self = Self(-2039);
94}
95
96// ===== Classification methods =====
97
98impl ErrorCode {
99    /// Authentication / signing failure. The request will not succeed without
100    /// fixing the API key, signature or timestamp.
101    ///
102    /// Covers: `UNAUTHORIZED` (-1002), `INVALID_TIMESTAMP` (-1021),
103    /// `INVALID_SIGNATURE` (-1022), `BAD_API_KEY_FMT` (-2014),
104    /// `REJECTED_MBX_KEY` (-2015).
105    pub fn is_auth(self) -> bool {
106        matches!(
107            self,
108            Self::UNAUTHORIZED
109                | Self::INVALID_TIMESTAMP
110                | Self::INVALID_SIGNATURE
111                | Self::BAD_API_KEY_FMT
112                | Self::REJECTED_MBX_KEY
113        )
114    }
115
116    /// Local clock drifted outside the server's `recvWindow`. Re-sync time
117    /// and retry. Code: `INVALID_TIMESTAMP` (-1021).
118    pub fn is_invalid_timestamp(self) -> bool {
119        self == Self::INVALID_TIMESTAMP
120    }
121
122    /// HMAC signature doesn't match. Usually a wrong API secret or a payload
123    /// that was modified after signing. Code: `INVALID_SIGNATURE` (-1022).
124    pub fn is_invalid_signature(self) -> bool {
125        self == Self::INVALID_SIGNATURE
126    }
127
128    /// API key format is invalid (length / characters). Code: -2014.
129    pub fn is_bad_api_key_format(self) -> bool {
130        self == Self::BAD_API_KEY_FMT
131    }
132
133    /// API key was rejected because it's been deleted/disabled, the source
134    /// IP isn't on its allowlist, or it lacks the permission this endpoint
135    /// needs. Binance reports all three as code -2015.
136    pub fn is_api_key_rejected(self) -> bool {
137        self == Self::REJECTED_MBX_KEY
138    }
139
140    /// The key lacks the permission required for this action. Same wire code
141    /// as [`Self::is_api_key_rejected`] (-2015); offered as a separate
142    /// predicate purely for caller intent.
143    pub fn is_wrong_permissions(self) -> bool {
144        self == Self::REJECTED_MBX_KEY
145    }
146
147    /// Request was malformed — missing/extra parameter, bad value, bad
148    /// symbol, etc. Range: -1199..=-1100.
149    ///
150    /// Product-specific codes outside this range (e.g. futures -4001) are
151    /// not covered; use [`Self::raw`] to handle them.
152    pub fn is_bad_request(self) -> bool {
153        (-1199..=-1100).contains(&self.0)
154    }
155
156    /// Rate-limit / throughput error. Caller is sending too much.
157    ///
158    /// Covers: `TOO_MANY_REQUESTS` (-1003), `TOO_MANY_ORDERS` (-1015),
159    /// `TOO_MANY_CONNECTIONS` (-1034), `TOO_MANY_MESSAGES` (-1181),
160    /// `TOO_MANY_SUBSCRIPTIONS` (-1191).
161    pub fn is_rate_limited(self) -> bool {
162        matches!(
163            self,
164            Self::TOO_MANY_REQUESTS
165                | Self::TOO_MANY_ORDERS
166                | Self::TOO_MANY_CONNECTIONS
167                | Self::TOO_MANY_MESSAGES
168                | Self::TOO_MANY_SUBSCRIPTIONS
169        )
170    }
171
172    /// Server-side problem; the request itself may have been valid.
173    ///
174    /// Covers: `UNKNOWN` (-1000), `DISCONNECTED` (-1001), `UNEXPECTED_RESP`
175    /// (-1006), `TIMEOUT` (-1007), `SERVER_BUSY` (-1008),
176    /// `SERVICE_SHUTTING_DOWN` (-1016).
177    pub fn is_server_error(self) -> bool {
178        matches!(
179            self,
180            Self::UNKNOWN
181                | Self::DISCONNECTED
182                | Self::UNEXPECTED_RESP
183                | Self::TIMEOUT
184                | Self::SERVER_BUSY
185                | Self::SERVICE_SHUTTING_DOWN
186        )
187    }
188
189    /// Worth retrying — server-side or rate-limit. Auth / bad-request errors
190    /// are not transient and will fail again on retry.
191    pub fn is_transient(self) -> bool {
192        self.is_server_error() || self.is_rate_limited()
193    }
194
195    /// Matching engine rejected the order (filter / price / quantity rules,
196    /// cancel-replace failure, amend rejected).
197    ///
198    /// Covers: `NEW_ORDER_REJECTED` (-2010), `CANCEL_REJECTED` (-2011),
199    /// `ORDER_AMEND_REJECTED` (-2038).
200    pub fn is_order_rejected(self) -> bool {
201        matches!(
202            self,
203            Self::NEW_ORDER_REJECTED | Self::CANCEL_REJECTED | Self::ORDER_AMEND_REJECTED
204        )
205    }
206
207    /// The referenced order ID doesn't exist (already filled/canceled or
208    /// never placed). Code: `NO_SUCH_ORDER` (-2013).
209    pub fn is_no_such_order(self) -> bool {
210        self == Self::NO_SUCH_ORDER
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::serde::deserialize_json;
218
219    #[test]
220    fn deserializes_from_bare_integer() {
221        let code: ErrorCode = deserialize_json("-1102").unwrap();
222        assert_eq!(code, ErrorCode::MANDATORY_PARAM_EMPTY_OR_MALFORMED);
223        assert_eq!(code.raw(), -1102);
224    }
225
226    #[test]
227    fn unknown_code_still_parses() {
228        // A code Binance might add tomorrow — must not break parsing.
229        let code: ErrorCode = deserialize_json("-9999").unwrap();
230        assert_eq!(code.raw(), -9999);
231        assert!(!code.is_auth());
232        assert!(!code.is_bad_request());
233        assert!(!code.is_transient());
234    }
235
236    #[test]
237    fn classification_predicates() {
238        assert!(ErrorCode::UNAUTHORIZED.is_auth());
239        assert!(ErrorCode::INVALID_SIGNATURE.is_auth());
240        assert!(ErrorCode::REJECTED_MBX_KEY.is_auth());
241        assert!(ErrorCode::REJECTED_MBX_KEY.is_wrong_permissions());
242        assert!(ErrorCode::REJECTED_MBX_KEY.is_api_key_rejected());
243
244        assert!(ErrorCode::BAD_SYMBOL.is_bad_request());
245        assert!(ErrorCode::MANDATORY_PARAM_EMPTY_OR_MALFORMED.is_bad_request());
246        assert!(!ErrorCode::UNKNOWN.is_bad_request());
247
248        assert!(ErrorCode::TOO_MANY_REQUESTS.is_rate_limited());
249        assert!(ErrorCode::TOO_MANY_REQUESTS.is_transient());
250
251        assert!(ErrorCode::SERVICE_SHUTTING_DOWN.is_server_error());
252        assert!(ErrorCode::SERVICE_SHUTTING_DOWN.is_transient());
253
254        assert!(ErrorCode::NEW_ORDER_REJECTED.is_order_rejected());
255        assert!(ErrorCode::NO_SUCH_ORDER.is_no_such_order());
256    }
257}