mode: 'agent'
Based on lessons learned from PR #28840 and reviewer feedback, these comprehensive instructions address the original migration requirements while preventing common issues.
async keyword to function declarationsreturn callback(err, result) with throw err or return resultreturn callback() with return (or return undefined)DO NOT wrap simple operations in try/catch just to tag errors with OError. With async/await, the stack trace is preserved automatically, making OError.tag less necessary for basic error propagation.
// OLD (callback style) - OError.tag was needed
callback(err) => {
OError.tag(err, 'description', { context })
return callback(err)
}
// BAD (unnecessary with async/await)
try {
await operation()
} catch (err) {
throw OError.tag(err, 'description', { context })
}
// GOOD (let errors propagate naturally)
await operation()
// ONLY use OError.tag when adding meaningful context or transforming errors
try {
await complexOperation()
} catch (err) {
if (err.code === 'SPECIFIC_ERROR') {
throw OError.tag(err, 'meaningful context about why this failed', {
important_context: value
})
}
throw err // let other errors propagate unchanged
}
Be extremely cautious when converting from serial to parallel operations. The original code's choice of sequential processing is often intentional.
// OLD - Sequential processing (often intentional)
Async.mapSeries(items, processItem, callback)
// BAD - Unbounded parallel processing
await Promise.all(items.map(processItem))
// BETTER - Keep sequential if unsure about resource limits
for (const item of items) {
await processItem(item)
}
// GOOD - Controlled batch processing for performance
const BATCH_SIZE = 10
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE)
await Promise.all(batch.map(processItem))
}
// IDEAL - Use Redis MGET for multiple key retrieval
// Instead of: Promise.all(keys.map(k => redis.get(k)))
const values = await redis.mget(keys)
When operations were called in the background (with empty callbacks), preserve this behavior:
// OLD - Background operation with ignored callback
someOperation(user, function () {}) // errors swallowed
// GOOD - Preserve background behavior
someOperation(user).catch(err => {
logger.error({ err }, 'Failed to run background operation')
})
// Or if truly fire-and-forget:
someOperation(user).catch(() => {}) // explicitly ignore errors
const { callbackifyAll } = require('@overleaf/promise-utils')
const MyModule = {
async myMethod(param) {
// async implementation
},
}
const moduleExports = {
...callbackifyAll(MyModule), // callback API
promises: MyModule, // promise API
}
module.exports = moduleExports
Only add method binding patterns when tests need to stub internal method calls:
// ONLY if tests need to stub internal calls to _internalMethod
MyModule._internalMethod = (...args) => moduleExports._internalMethod(...args)
Do NOT expose internal methods at the top level - they should be accessible via moduleExports.promises._internalMethod.
// OLD
it('should do something', function (done) {
MyModule.method(param, function (err, result) {
expect(err).to.not.exist
expect(result).to.equal(expected)
done()
})
})
// NEW
it('should do something', async function () {
const result = await MyModule.promises.method(param)
expect(result).to.equal(expected)
})
// For Redis or database mocks, ensure method chaining works
beforeEach(function () {
redis.multi = sinon.stub().returns({
sadd: sinon.stub().returnsThis(),
pexpire: sinon.stub().returnsThis(),
exec: sinon.stub().resolves(),
})
})
// Correct pattern for Redis multi operations
const multi = redis.multi()
multi.sadd(key, value)
multi.pexpire(key, ttl)
await multi.exec()
// BAD - Multiple individual operations
const values = await Promise.all(keys.map(k => redis.get(k)))
// GOOD - Use batch operations when available
const values = await redis.mget(keys)
ALWAYS run tests early and often during migration. Don't wait until the end.
Common test running problems:
docker system prune -fMOCHA_GREP="ModuleName" make test_unit_appmulti() must return {method: stub().returnsThis(), ...})When methods call other methods internally, tests may need to stub those calls:
// If methodA() calls methodB() internally and tests need to verify this:
// DON'T do this - creates unnecessary complexity:
const selfRef = { ... }; // BAD pattern
// DO this - route through the module exports interface:
moduleExports.promises.methodB(params).catch(err => {
logger.error({ err }, 'Failed to run background operation')
})
// Add a brief comment explaining the routing pattern:
// Route through moduleExports so tests can stub this call
The selfRef pattern should be avoided - it's a code smell that indicates better module structure is needed:
// BAD - selfRef pattern
const selfRef = {}
selfRef.methodA = async function() {
await selfRef.methodB() // circular reference
}
// GOOD - route through module exports when stubbing is needed
async methodA() {
await moduleExports.promises.methodB() // testable
}
Remove ALL legacy CoffeeScript artifacts - this is a required part of the migration.
Look for and remove patterns like these (exact format may vary):
/* eslint-disable */
// TODO: This file was created by bulk-decaffeinate.
/* eslint-disable
camelcase,
n/handle-callback-err,
max-len,
no-return-assign,
no-unused-vars,
*/
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Search patterns to look for:
/* eslint-disable */ blocks at the top of filesCheck both implementation AND test files for these artifacts.
When you need to write "ugly" code for unavoidable technical reasons, add a brief comment explaining why:
// Background operation - preserve fire-and-forget behavior
// Route through moduleExports so tests can stub this call
moduleExports.promises._checkSessions(user).catch(err => {
logger.error({ err }, 'Failed to check sessions in background')
})
This prevents future developers from "refactoring" the code and breaking functionality.
With async/await, many OError.tag() wrappers can be removed. OError.tag() was primarily used to preserve stack traces across callback boundaries, but async/await handles this automatically.
// BEFORE - callback era (needed OError.tag for stack traces)
try {
await redis.multi().sadd(key, value).exec()
} catch (err) {
throw OError.tag(err, 'error adding to redis set', { key })
}
// AFTER - async/await preserves stack traces naturally
await redis.multi().sadd(key, value).exec()
Keep OError.tag() only when:
Remove OError.tag() when:
These instructions should be applied systematically, with careful consideration of the specific context and requirements of each module being migrated.