Hunting the Bug That Lurked at 12:02 AM
I recently took over the frontend team’s service stability governance and encountered some very distinctive production issues. Here I’ll document one particularly “interesting” bug hunt.
Background
The story began with a mention from my boss:

I investigated at the time but couldn’t find any leads, so I gave up (thinking it was intermittent and would fix itself in a few days!). Until one day I received the same alert again and realized it had been happening for several days.
Looking up:

I panicked—can’t let this become an incident! So began my debugging journey.
Debugging Approach
The alert error stack showed this was an “unhandledRejection”:
1 | [ERROR][2020-09-16T23:59:59.582+0800][default:process.<anonymous> at /home/xxx/xxx/xxx/lib/app.js:49:10] _undef||traceid=64594b155f6231298ae0e2b114a1a02||spanid=38197e8a96a6d96a||pid=1431||msg=on unhandledRejection, error: { Error: ERR invalid expire time in set |
Tracing up layer by layer didn’t reveal the source! After several unsuccessful guesses, I went through the alert group history and discovered a stunning pattern—whenever this alert appeared, it was always at exactly 00:02 AM:

So the debugging focus narrowed to:
- Is there a daily cron job setting a Redis value’s expiration time?
- Is there a server time discrepancy?
Debugging Process
Spoiler: it was neither of those reasons.
After extensive code searching, I found several places that set Redis values. Combined with Google searches, someone pointed out that Redis expiration time cannot be less than 0. Local verification confirmed that expiration time cannot be 0:

The investigation shifted to where the code could produce an expiration time less than 0. The suspicious code:
1 | setRedisKey( |
Could the value 24*3600 - getPastTimeOfToday() / 1000 be 0 or negative? Let’s look at the full logic:
1 | const getPastTimeOfToday = () => { |
This value represents how many seconds remain until today ends. It shouldn’t be negative. I even wondered if line 2 executed yesterday while line 7 executed today, which would make the function return a value greater than 24*3600, making 24*3600 - getPastTimeOfToday() / 1000 negative. But that’s unlikely. So I went to the server to reproduce the bug:
1 | const { setRedisKey } = require('./lib/xxx/xxx/redis') |
The result matched the alert error stack:

Having reproduced the error, I dug deeper into setRedisKey:
1 | const setRedisKey = (key, value, expireTime = DEFAULT_EXPIRETIME) => { |
The expiration time expireTime is wrapped with Math.floor, meaning when 0 < expireTime < 1, Math.floor(expireTime) equals 0. So when the server time approaches 00:00:00, getPastTimeOfToday returns (24*3600-x)*1000, and since it’s very close to 00:00:00, x is between 0 and 1 (imagine when the “remaining milliseconds of the day” are less than 1000).
Why unhandledRejection
Why wasn’t this error caught? If it had been caught from the start with a complete error stack, debugging would have been much smoother. The business code (sanitized):
1 | Promise.resolve().then(() => { |
Did you spot it? The Promise in then isn’t returned. But would returning it definitely catch it? In this pattern, yes. But written like this, it still causes unhandledRejection:
1 | new Promise((resolve, reject) => { |
Why? Because you need to manually call reject to throw errors, which requires passing resolve and reject layer by layer into potentially failing Promises. Cherish life, stay away from raw Promises—use async/await syntax instead (for equivalent functionality).
Solution
Now that we’ve found the cause, the fix is clear:
- Change Math.floor (floor) to Math.ceil (ceiling)—though in extreme cases, remaining milliseconds could be exactly 0
- When
expireTimeis <= 0, assign it to 1 (one more second, haha), and log a warning - Check for unreturned Promises and return them uniformly
I tried finding a TypeScript way to constrain number to positive integers but couldn’t find one. If anyone knows how, please share!
Hunting the Bug That Lurked at 12:02 AM
http://quanru.github.io/2020/09/18/Hunting-the-Bug-That-Lurked-at-0002-AM

