Pinned Tweet
Replying to @TTrimoreau
none of them honestly. i'd rather keep shipping for five clients than bet a year on one idea i didn't pick myself.
a user in Sydney wrote in saying we billed them a day early
they signed up at 9am on November 1st. their receipt said October 31st
both were right
9am in Sydney that day was 10pm on the 31st in UTC, and the server stored and printed everything in UTC
the charge went out on time. the date on the receipt was just 11 hours behind their life
fixed it in 2 lines. keep the instant in UTC, show it in the user's timezone
a date without a timezone is a guess with nice formatting
split a $100 plan into 3 monthly payments
33.33, 33.33, 33.33
looked fine on every invoice. the accountant found it 4 months later
each customer paid 99.99. one cent short, every time. across 1,800 customers that's $18 we never billed
no single invoice looked wrong, which is why it lasted 4 months
the fix was tiny. round the first two payments, then make the last one whatever is left. 33.33, 33.33, 33.34
splitting money is easy. splitting it so it adds back up is the actual work
a user said the signup form rejected their email on the second try
first click: valid. second click: invalid. third click: valid again
same email, same code, alternating answers
the validator was one shared regex with the g flag on it
with g set, test() remembers where the last match ended in lastIndex and starts the next search from there. so the second call began past the end of the string and found nothing
took the g off and it worked on all 3 clicks
a regex that keeps state between calls is a bug waiting for the second click
a migration script printed "done" 0.3 seconds after I ran it
it was supposed to update 4,000 user rows
the loop was users.forEach(async (u) => await save(u))
forEach ignores whatever your callback returns, promises included. it fired off 4,000 saves, moved straight to the next line and closed the database connection
a few seconds later the first "connection closed" error took the whole process down. a handful of rows made it, the rest never got touched
a plain for...of loop fixed it
forEach is fine right up until the word async shows up inside it
added a meeting to Monday and it showed up on all 7 days of the week
the grid was built with Array(7).fill([])
looked fine. 7 empty arrays, one per day
except fill doesn't make 7 arrays. it makes one array and puts the same reference in all 7 slots, so pushing to Monday pushed to Sunday too
took 40 minutes to find because my tests only ever added one meeting and checked one day
Array.from({ length: 7 }, () => []) fixed it
fill is great for zeros and terrible for anything you plan to change later
a user wrote in 4 times saying they couldn't remove their phone number
clear the box, hit save, green "saved" toast. reload. the number was back
the form set phone to undefined when the field was empty. JSON.stringify quietly drops any key whose value is undefined, so the PATCH body never mentioned phone at all
the server saw nothing to change and kept the old one
swapped undefined for null and it worked first try
null means "make this empty". undefined means "I didn't say anything". JSON only has a word for one of them
a user couldn't save their display name. "too long", the form said
the limit was 20 characters. they had typed 12
9 letters and 3 flag emojis
in JavaScript a flag is two regional indicator symbols, and each one takes 2 UTF-16 units. so a single flag has a length of 4
9 + 12 = 21
the fix was counting with Intl.Segmenter, which counts what a person actually sees on the screen
"how long is this string" has at least three right answers, and the default one is the wrong one for humans
my sync script said a Notion database had exactly 100 rows
it had 1,342
no error, no warning. status 200 and a tidy list of 100 pages, so I believed it for 3 weeks
then someone asked why most of their entries never showed up anywhere
the API returns 100 at a time with a has_more flag and a next_cursor for the page after. I never checked has_more
the fix was a 6 line while loop
now any count that lands on exactly 100 makes me nervous before it makes me happy
a customer couldn't pay for a $19.99 order. the $9.99 and $29.99 ones went through fine
Stripe kept rejecting it with "invalid integer"
I was sending the amount as price * 100
19.99 * 100 in JavaScript is 1998.9999999999998
floats can't store most decimals exactly, so a few prices land a hair under the cent. 9.99 and 29.99 happen to round clean, which made it look random
Math.round fixed it in one line
storing money as integer cents from day one would have meant nothing to fix
set a Christmas sale to start with new Date(2026, 12, 25)
tested it by faking the clock to December 25th. no banner
faked it to January 25th. banner
JavaScript months start at 0. January is 0, December is 11. so 12 isn't an error, it just rolls over into the next year
no warning, no exception. a perfectly valid date, one month late
the fix was typing 11 and feeling a bit stupid
the calendar has 12 months. JavaScript has 0 through 11 and a lot of confidence
a user wrote in asking why they had two accounts with the same email
they didn't, technically. one was all lowercase. the other started with a capital P because their phone capitalised it at signup
my unique constraint on email did its job. Postgres compares text exactly, so P and p are two different people
38 users had done the same thing over 2 years
the fix was a unique index on lower(email) and a very boring merge script
the database was right all along. humans just never type the same thing twice
my signups chart showed a spike at 1am this morning. about double a normal hour
no launch, no tweet, nothing. I went looking for a bot
it was the clocks. the UK went back an hour at 2am, so 1:00 to 2:00 happened twice
the chart grouped by local hour, so two hours of signups landed in one bar
the fix was boring. bucket everything in UTC and convert only the labels
nothing was wrong with the data. the day just had 25 hours in it
wrote a retry wrapper for a flaky API. 3 attempts, exponential backoff, the works
it never retried once
the API returned 503s for most of an afternoon and my logs said success
fetch only rejects when the network itself fails. a 500 or a 503 is a perfectly good response as far as fetch is concerned
my try/catch was waiting for an error that was never coming
one line fixed it: if (!res.ok) throw new Error(res.status)
the retry logic was fine. nobody ever told it something went wrong
a leaderboard I wrote put someone with 9 points in first place, ahead of someone with 100
the code was one line. scores.sort().reverse()
JavaScript's default sort compares everything as strings
so [9, 10, 100] sorts to [10, 100, 9]. "9" lands after "1" the same way "z" lands after "a"
every test passed because every test score had two digits
the fix was scores.sort((a, b) => b - a)
it took 4 minutes to fix and 3 weeks for anyone to notice
wrote a query to find users who never placed an order
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders)
0 rows. we had 4,000 users and maybe 1,200 had ever ordered
no error. the query was fine. the data wasn't
one guest checkout in orders had a NULL user_id
NOT IN checks id <> each value in the list. id <> NULL isn't true or false, it's unknown, so not a single row gets through
NOT EXISTS skips the NULL and gave back all 2,800
one NULL in one row erased the whole answer
I scheduled a reminder email for 30 days out with setTimeout
it went out 4 milliseconds later. to 40 people
setTimeout keeps the delay in a 32-bit signed int. the biggest it takes is 2,147,483,647 ms, about 24.8 days
go past that and Node sets the delay to 1. it does print a warning, on stderr, which nobody was reading
30 days is 2,592,000,000 ms. too big by about 5 days
now anything longer than a few minutes is a database row with a due date and a job that checks it
a timer is not a calendar
my script saved tweet IDs to a JSON file and every lookup after that came back 404
the IDs looked fine. 19 digits, same length as on the site
they weren't. 2113619830578241578 went in and 2113619830578241500 came out
JSON.parse turns every number into a JS double, and a double only holds whole numbers exactly up to 9,007,199,254,740,991. past that it rounds and says nothing
no error. just a real looking ID pointing at a tweet that doesn't exist
now IDs stay strings from the first fetch to the last write
there's a reason the old API sent id_str right next to id
wanted a job to run on the last day of each month
wrote 0 0 31 * * and moved on
end of April came. nothing ran. no error, cron just never saw a 31st
I only noticed 9 days later when the April report still hadn't shown up
only 7 months have one. the job had been skipping 5 months of the year
the fix was boring. run at 00:05 on the 1st and process the month that just ended
cron has no idea what "end of month" means. it only knows the numbers you hand it
an export job pulled 2 million rows, 500 at a time, and got slower with each page
page 1: 20ms
page 1,000: 1.4s
page 4,000: 9s
the query was ORDER BY id LIMIT 500 OFFSET n
OFFSET doesn't skip rows. it reads them and throws them away, so the last page read 2 million rows to hand back 500
switched to WHERE id > last_seen_id LIMIT 500
each page stayed at 20ms and the whole export finished in 4 minutes
OFFSET is a loop you didn't know you wrote
set a cron job to send me a daily report at 9am
first morning, nothing in the inbox
10am, still nothing. logs were clean, no failed run
then at 2:30pm it showed up
the server clock was UTC. India sits 5 hours 30 minutes ahead, so "0 9" meant 9am UTC, which is 2:30pm here
the fix was "30 3" instead. and since India never moves its clocks for summer, it stays right all year
a scheduled job has two times, the one you meant and the one the server heard
