WeChat Promise Wrapper Resolves [object Object]? Take .data
Tapping the photo-recognition button in our mini program failed every single time — same on devtools and real devices, regardless of which image was sent. The server only ever said "image recognition failed, please try again later."
I hit this while building Life, an AI bookkeeping assistant — photo recognition is one of its quick capture entry points. The fix turned out to be one line; the hard part was the hunt, because every error message along the chain was lying.
TL;DR
- Root cause: our own promise wrapper for wx APIs used
success: resolve, which resolves the entire callback result — andwx.readFile's success result is{ data, errMsg }as one object, not the data itself. Downstream,String(b64)turned the whole object into"[object Object]"; after stripping whitespace, 14 junk characters of fake base64 decoded into 9 bytes of fixed garbage and got shipped to the server. - Fix: dot into the resolved object (
res.data) and throw on non-string values. - Hunting tip: when the error body is generic ("invalid params") and leaks no details, add shape diagnostics on the send side (length / charset / magic bytes, zero content) instead of running more format-guessing experiments.
The symptom: 100% failure, independent of image content
A failure that is 100% reproducible and ignores image content points at the data on the send side — not at the recognition model.
The pipeline is: pick image → compress → read as base64 → POST to our server → forwarded to a vision model. The failure pattern was perfectly uniform: devtools matched real devices, every attempt failed. The server log showed only a 400 from the vision gateway:
POST /vision 400 {"error": "invalid params"}
invalid params is a generic body — it doesn't say which parameter or how it was wrong. Our routing layer wraps any gateway failure into a 503 "image recognition failed, please try again later", so users and logs were equally uninformed.
Two dead ends, both caused by the generic error
Generic error bodies steer debugging toward format guessing — both of our dead ends started exactly there.
Guess one: non-standard mime type. We first suspected the compressed image used image/jpg (the standard spelling is image/jpeg). Hitting the gateway directly with all three mime variants returned 200 every time — disproven. The image and its metadata were fine; the problem was further downstream.
Guess two: base64 line breaks. Next we suspected wx.readFile produced base64 with newlines that the gateway rejected. Injecting \n into a test request reproduced the same error body — note the false positive: a generic error means every illegal payload looks identical, so reproducing it proves nothing about what's actually on the wire. We shipped whitespace stripping on both ends anyway (.replace(/\s+/g, '')), retested — still 100% failure. Guess two disproven; the stripping stayed as harmless defense.
Both guesses failed for the same reason: they assumed the payload was a malformed image, when in reality what we were sending was not an image at all.
Root cause: the wrapper resolved the whole callback result object
wx.readFile's success result is one object, { data, errMsg }, and the wrapper resolved it whole — what reached the downstream code was never base64.
With guesses exhausted, we switched tactics: add shape diagnostics on the server's failure path — metadata only (length, mime, whitespace, charset, decoded byte count, magic hex), never content, zero privacy risk. The next reproduction spat out the truth:
b64Len: 14 charsetOk: false decodedBytes: 9 magicHex: a1b8de72
A real image's base64 is tens of thousands of characters; this was 14. It decoded to 9 bytes of fixed garbage, and the magic bytes were identical on every request. One more clue locked it in: payloads from two different images were byte-for-byte identical — a fixed string, not an image. And a 14-character fixed string is exactly "[objectObject]": "[object Object]" with the space stripped.
Back in the code, every link in the chain looked reasonable:
// Generic wrapper: callback-style API → Promise
const p = (fn) => (opts) =>
new Promise((resolve, reject) => fn({ ...opts, success: resolve, fail: reject }))
// Read file as base64 (before the fix)
const readFileBase64 = (filePath) =>
p(wx.getFileSystemManager().readFile.bind(wx.getFileSystemManager()))({
filePath,
encoding: 'base64',
}).then((b64) => String(b64))
wx.readFile's success callback result is one object: { data, errMsg }. The wrapper's success: resolve resolved that whole object; .then((b64) => String(b64)) received the object and String() coerced it into "[object Object]" — zero errors anywhere. Strip, encode, ship — everything flowed until the gateway bounced it with a generic 400.
Node's base64 decoding even ignores illegal characters ([ and ]), so the 12 legal base64 characters decoded into exactly 9 bytes of garbage — matching the diagnostic log digit for digit.
The fix: dot into the value, throw on non-strings
The fix is two things: dot into the resolved object (res.data), and throw when the value isn't a non-empty string.
const readFileBase64 = (filePath) =>
p(wx.getFileSystemManager().readFile.bind(wx.getFileSystemManager()))({
filePath,
encoding: 'base64',
}).then((res) => {
const data = res && res.data
if (typeof data !== 'string' || !data) throw new Error('failed to read image')
return data
})
The guard pays off the moment bad data appears: the client gets a clear message instead of a generic 400 from far away. The whole fix was +8/-1 lines in the commit, comment included.
After the fix, recognition worked on devtools and real devices alike, and the 100% failure rate disappeared.
Watch out
When writing promise wrappers for wx APIs, always dot into what you resolve — callback result shapes differ per API: .data for readFile, .tempFiles for chooseMedia, .tempFilePath for compressImage. Confirm each one; never write a "universal then" that assumes a shape. And String(someObject) always silently yields "[object Object]" — no throw, no warning. Against generic "invalid params" errors, one shape-diagnostic log line (length / charset / magic bytes only) beats any number of guess-and-retest rounds.
FAQ
Why does my WeChat mini program image recognition always fail?
A 100% failure rate that ignores image content points at the send side, not the recognition model. Instrument the sender with shape diagnostics — payload length, decoded byte count, magic bytes — never the content itself. That's how we found our 'base64' was a 14-character junk string.
Why does my promise wrapper return [object Object]?
Because success: resolve passes the entire callback result object ({ data, errMsg }) as the resolved value, and String() on an object yields "[object Object]" silently — no error, no warning. Dot into the result (res.data) and throw when the value is not a non-empty string.
How do I wrap wx callback APIs as promises safely?
Resolve the whole result, then pick the documented property per API — .data for readFile, .tempFilePath for compressImage; there is no universal shape. Add a guard that throws on missing or non-string values so bad data dies at the source instead of surfacing as a generic gateway 400.
CCLEE
Independent developer, 24 years in e-commerce, focused on grounding AI in real business scenarios.
Work with me