Перейти к основному содержимому

СОБЕСЕДОВАНИЕ в ЯНДЕКС на Frontend-разработчика

· 17 мин. чтения

Сегодня мы разберём техническое собеседование на позицию фронтенд-разработчика в Яндекс, на котором кандидат решал задачи на JavaScript — от теоретических вопросов о типах данных и работе промисов до практической реализации методов массивов, функции retry и обработки асинхронных запросов с таймаутом. Собеседование проходило в формате живого кодирования: кандидат рассуждал вслух, допускал ошибки, находил их самостоятельно и под направляющими вопросами интервьюера приходил к рабочим решениям, демонстрируя понимание ключевых концепций языка и умение работать с прототипами, замыканиями и асинхронностью.

Вопрос 1. Что будет выведено в консоль для следующих выражений: typeof [], typeof null, [1] + [2], +[1,2], console.log(1), setTimeout(() => console.log(2), 0), console.log(3), var a = 2; var b = a; b++; console.log(a); console.log(b); var c = [1,2,3]; var d = c; d.push(4); console.log(c); console.log(d); console.log(x); var x = 10; console.log(x); console.log(y); const y = 10; console.log(y)?

Таймкод: 00:03:58

Ответ собеседника: Неполный. typeof [] выведет object, потому что массив является наследником object. typeof null тоже выведет object — это древний баг JavaScript. [1] + [2] выведет строку '12', потому что число приведётся к строке. +[2,2] кандидат сначала сказал, что будет ошибка, но позже исправился на 2 (число), хотя правильный ответ — NaN. console.log(1) выведет 1, console.log(3) выведет 3, setTimeout(() => console.log(2), 0) выведет 2 после синхронного кода, так как это макротаска. var a = 2; var b = a; b++; console.log(a) выведет 2, console.log(b) выведет 3, так как копирование примитивов происходит по значению. var c = [1,2,3]; var d = c; d.push(4); console.log(c) выведет [1,2,3,4], console.log(d) выведет [1,2,3,4], так как копирование происходит по ссылке. console.log(x) выведет undefined, а не ошибку, потому что var всплывает, но значение присвоится только в момент присваивания. console.log(x) после присваивания выведет 10. console.log(y) выведет ошибку (ReferenceError), потому что const находится в временной мёртвой зоне. Далее console.log(y) не выполнится из-за ошибки.

Правильный ответ:

Давайте разберём каждое выражение по порядку.

1. typeof []

Результат: "object". Массив в JavaScript — это специализированный тип объекта. Оператор typeof для массивов возвращает "object", так как массивы наследуют от Object. Это не баг, а особенность системы типов JavaScript: typeof отличает только примитивы и функцию ("function"), всё остальное — "object".

2. typeof null

Результат: "object". Это действительно исторический баг в JavaScript, сохранённый для обратной совместимости. При первоначальной реализации значения в JavaScript представлялись как тег типа и значение. Тег для объектов был 0, а null представлялся как нулевой указатель (0x00), поэтому проверка типа возвращала тот же тег, что и для объектов.

3. [1] + [2]

Результат: "12" (строка). Оператор + при работе с массивами вызывает неявное приведение типов (type coercion). Сначала массивы преобразуются к примитиву через метод toString(): [1].toString() даёт "1", [2].toString() даёт "2". Затем происходит конкатенация строк: "1" + "2" = "12".

4. +[1,2]

Результат: NaN (Not a Number). Унарный оператор + пытается преобразовать операнд в число. Массив [1,2] сначала преобразуется к строке через toString(), получается "1,2". Затем "1,2" преобразуется к числу, что даёт NaN, так как "1,2" не является валидным числовым литералом. Важно понимать разницу: +[1] даст 1 (массив с одним элементом успешно преобразуется к числу), а +[1,2] даст NaN.

5. Порядок выполнения console.log(1), setTimeout(() => console.log(2), 0), console.log(3)

Результат: 1, 3, 2. Это демонстрирует работу event loop в JavaScript:

  • console.log(1) — синхронный вызов, выполняется сразу, выводит 1.
  • setTimeout(() => console.log(2), 0) — регистрирует колбэк в очереди макротасок (macrotask queue). Даже с задержкой 0 мс колбэк выполнится только после завершения всего синхронного кода.
  • console.log(3) — синхронный вызов, выполняется сразу, выводит 3.
  • После завершения синхронного кода event loop берёт колбэк из очереди макротасок и выполняет console.log(2), выводит 2.

6. var a = 2; var b = a; b++; console.log(a); console.log(b)

Результат: 2, 3. При присваивании var b = a происходит копирование значения примитива. Переменные a и b хранят независимые копии значения 2. Операция b++ увеличивает b до 3, но не влияет на a. Это поведение характерно для примитивных типов (number, string, boolean, null, undefined, symbol, bigint) — они копируются по значению.

7. var c = [1,2,3]; var d = c; d.push(4); console.log(c); console.log(d)

Результат: [1,2,3,4], [1,2,3,4]. При присваивании var d = c копируется не сам массив, а ссылка на него. Обе переменные c и d указывают на один и тот же объект в памяти. Метод d.push(4) мутирует этот объект, поэтому изменения видны через обе переменные. Это поведение характерно для ссылочных типов (объекты, массивы, функции) — они копируются по ссылке.

8. console.log(x); var x = 10; console.log(x)

Результат: undefined, 10. Объявления через var поднимаются (hoisting) в начало своей области видимости (глобальной или функциональной), но инициализация остаётся на месте. Фактически код интерпретируется так:

var x; // объявление поднято, x = undefined
console.log(x); // undefined
x = 10; // присваивание значения
console.log(x); // 10

Важно: var поднимает объявление, но не присваивание. Поэтому до строки x = 10 переменная существует, но содержит undefined.

9. console.log(y); const y = 10; console.log(y)

Результат: ReferenceError: Cannot access 'y' before initialization, далее выполнение прекращается. Переменные, объявленные через constlet), также поднимаются, но попадают в временную мёртвую зону (Temporal Dead Zone, TDZ) — область от начала блока до строки объявления. Попытка обратиться к переменной в TDZ вызывает ReferenceError. Это принципиально отличается от var, где обращение до объявления даёт undefined.

Ключевое отличие:

  • var: поднимается с инициализацией undefined, доступна до объявления.
  • let/const: поднимается без инициализации, недоступна до объявления (TDZ).

Итоговый вывод всего скрипта:

object
object
12
NaN
1
3
2
2
3
1,2,3,4
1,2,3,4
undefined
10
ReferenceError: Cannot access 'y' before initialization

Важные концепции для запоминания:

  • Type coercion — неявное приведение типов, особенно с оператором + и унарными операторами.
  • Event loop — макротаски (setTimeout, setInterval) выполняются после синхронного кода и микротасок (Promise).
  • Примитивы vs ссылочные типы — примитивы копируются по значению, объекты — по ссылке.
  • Hoistingvar поднимается с undefined, let/const поднимаются с TDZ.

Вопрос 2. Реализовать метод groupBy для массива, который группирует элементы массива в объект по ключу, возвращаемому callback-функцией.

Таймкод: 00:10:23

Ответ собеседника: Правильный. Кандидат реализовал метод groupBy, используя прототип Array. Внутри метода использовал цикл map для прохода по элементам массива, вызывал callback для получения ключа, проверял наличие ключа в результирующем объекте и добавлял элемент в соответствующий массив. Сначала столкнулся с ошибкой из-за использования стрелочной функции (this ссылался на window), но потом исправил на обычную функцию. Также обсудили оптимизацию кода и разницу между map и forEach.

Правильный ответ:

Вот несколько вариантов реализации groupBy — от базового до продвинутого.

1. Базовая реализация через Array.prototype

Array.prototype.groupBy = function(callback) {
const result = {};

for (let i = 0; i < this.length; i++) {
const key = callback(this[i], i, this);

if (!result[key]) {
result[key] = [];
}

result[key].push(this[i]);
}

return result;
};

// Пример использования
const people = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 25 },
{ name: 'David', age: 30 }
];

const grouped = people.groupBy(person => person.age);
console.log(grouped);
// {
// '25': [{ name: 'Alice', age: 25 }, { name: 'Charlie', age: 25 }],
// '30': [{ name: 'Bob', age: 30 }, { name: 'David', age: 30 }]
// }

2. Реализация через reduce

Array.prototype.groupBy = function(callback) {
return this.reduce((acc, item, index, array) => {
const key = callback(item, index, array);
(acc[key] = acc[key] || []).push(item);
return acc;
}, {});
};

3. Реализация без мутации прототипа (рекомендуется)

Расширение встроенных прототипов считается антипаттерном — может конфликтовать с другими библиотеками и будущими стандартами. Лучше создать утилитарную функцию:

function groupBy(array, callback) {
const result = {};

for (const item of array) {
const key = callback(item);
(result[key] = result[key] || []).push(item);
}

return result;
}

// Или через reduce
const groupBy = (array, callback) =>
array.reduce((acc, item) => {
const key = callback(item);
(acc[key] = acc[key] || []).push(item);
return acc;
}, {});

4. Нативный метод Object.groupBy (ES2024)

Современные движки уже поддерживают нативный метод:

const people = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 }
];

const grouped = Object.groupBy(people, person => person.age > 27 ? 'adult' : 'young');
// { young: [{ name: 'Alice', age: 25 }], adult: [{ name: 'Bob', age: 30 }] }

Важные нюансы реализации:

Проблема с this в стрелочных функциях. Стрелочные функции не имеют собственного this — они захватывают его из лексического окружения. Если использовать стрелочную функцию в методе прототипа, this будет ссылаться на глобальный объект (или undefined в strict mode), а не на массив.

// Неправильно — this не ссылается на массив
Array.prototype.groupBy = (callback) => {
console.log(this); // window или undefined
};

// Правильно — обычная функция
Array.prototype.groupBy = function(callback) {
console.log(this); // массив, на котором вызван метод
};

Выбор между map, forEach и циклом for.

  • map — создаёт новый массив результатов, подходит когда нужно преобразовать элементы. Для groupBy не подходит идеально, так как результат — объект, а не массив.
  • forEach — выполняет побочные эффекты для каждого элемента, не создаёт промежуточный массив. Подходит лучше, чем map.
  • for/for...of — самый производительный вариант, позволяет использовать break/continue.
  • reduce — функциональный подход, лаконичный, но может быть менее читаемым.

Оптимизация проверки существования ключа.

// Вариант 1: явная проверка
if (!result[key]) {
result[key] = [];
}
result[key].push(item);

// Вариант 2: тернарный оператор
(result[key] = result[key] || []).push(item);

// Вариант 3: с nullish coalescing (для ключей, которые не могут быть falsy)
(result[key] ??= []).push(item);

Оператор ??= (logical nullish assignment) присваивает значение только если текущее значение null или undefined, что безопаснее чем ||, который считает falsy значениями 0, "", false.

Вопрос 3. Реализовать метод fetchWithRetry, который выполняет GET-запрос по URL с помощью fetch и делает до 5 повторных попыток в случае ошибки, используя только Promise API (без async/await).

Таймкод: 00:19:50

Ответ собеседника: Правильный. Кандидат реализовал метод fetchWithRetry с использованием рекурсии. Метод возвращает Promise, принимает URL и количество оставшихся попыток (по умолчанию 5). Внутри выполняется fetch-запрос, при успехе данные парсятся как JSON и возвращаются через resolve. При ошибке, если попытки исчерпаны, вызывается reject, иначе — рекурсивный вызов с уменьшенным счётчиком. Обсудили, что fetch и response.json() возвращают Promise, а также возможность обработки ошибок вторым колбэком в .then().

Правильный ответ:

Вот несколько вариантов реализации fetchWithRetry с использованием только Promise API.

1. Базовая реализация с рекурсией

function fetchWithRetry(url, retries = 5) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.catch(error => {
if (retries <= 0) {
throw new Error(`Failed after all retries: ${error.message}`);
}
return fetchWithRetry(url, retries - 1);
});
}

// Пример использования
fetchWithRetry('https://api.example.com/data')
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error.message));

2. Реализация с задержкой между попытками (exponential backoff)

function fetchWithRetry(url, retries = 5, delay = 1000) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.catch(error => {
if (retries <= 0) {
throw error;
}

return new Promise(resolve => setTimeout(resolve, delay))
.then(() => fetchWithRetry(url, retries - 1, delay * 2));
});
}

3. Реализация с явным созданием Promise

function fetchWithRetry(url, retries = 5) {
return new Promise((resolve, reject) => {
function attempt(remainingRetries) {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(resolve)
.catch(error => {
if (remainingRetries <= 0) {
reject(new Error(`All retries exhausted: ${error.message}`));
} else {
attempt(remainingRetries - 1);
}
});
}

attempt(retries);
});
}

4. Продвинутая реализация с конфигурацией

function fetchWithRetry(url, options = {}) {
const {
retries = 5,
delay = 1000,
backoffFactor = 2,
retryOn = [408, 429, 500, 502, 503, 504]
} = options;

return fetch(url)
.then(response => {
if (!response.ok) {
const error = new Error(response.statusText);
error.status = response.status;
throw error;
}
return response.json();
})
.catch(error => {
const shouldRetry = retries > 0 &&
(!error.status || retryOn.includes(error.status));

if (!shouldRetry) {
throw error;
}

return new Promise(resolve => setTimeout(resolve, delay))
.then(() => fetchWithRetry(url, {
...options,
retries: retries - 1,
delay: delay * backoffFactor
}));
});
}

// Пример использования
fetchWithRetry('https://api.example.com/data', {
retries: 3,
delay: 500,
backoffFactor: 2,
retryOn: [500, 502, 503]
});

Важные концепции и нюансы:

Почему fetch не отклоняет Promise при HTTP-ошибках. Это частый источник ошибок. fetch отклоняет Promise только при сетевых ошибках (нет соединения, DNS-ошибка и т.д.). При HTTP-статусах 4xx и 5xx fetch резолвится успешно, но свойство response.ok будет false. Поэтому необходима явная проверка:

// Неправильно — не ловит HTTP-ошибки
fetch(url)
.then(response => response.json())
.catch(error => console.log('Ошибка только при сетевых проблемах'));

// Правильно — проверяем response.ok
fetch(url)
.then(response => {
if (!response.ok) throw new Error(response.status);
return response.json();
})
.catch(error => console.log('Ловим и сетевые, и HTTP-ошибки'));

Разница между .then(onFulfilled, onRejected) и .then().catch().

// Вариант 1: второй аргумент then НЕ ловит ошибки из onFulfilled
fetch(url)
.then(
response => { throw new Error('Ошибка в then'); },
error => console.log('Не выполнится!') // не поймает ошибку выше
);

// Вариант 2: catch ловит ошибки из всей цепочки
fetch(url)
.then(response => { throw new Error('Ошибка в then'); })
.catch(error => console.log('Выполнится!', error)); // поймает ошибку

Exponential backoff. При повторных запросах важно увеличивать задержку между попытками, чтобы не перегружать сервер. Формула: delay * backoffFactor^attempt. Например, при delay=1000 и backoffFactor=2: 1с, 2с, 4с, 8с, 16с.

Идемпотентность. Retry-логика безопасна только для идемпотентных операций (GET, PUT, DELETE). POST-запросы могут создавать дубликаты при повторных попытках, если сервер обработал первый запрос, но ответ не дошёл до клиента.

Вопрос 4. Реализовать функцию, которая проверяет, является ли строка панограммой (содержит все 26 латинских символов хотя бы один раз).

Таймкод: 00:40:56

Ответ собеседника: Правильный. Кандидат реализовал функцию проверки панограммы с использованием Set. Создал массив letters, содержащий все 26 латинских букв. Проходит по каждому символу входной строки, приводит его к верхнему регистру и проверяет, находится ли он в диапазоне от первой до последней буквы массива letters. Если символ является буквой, добавляет его в Set. В конце проверяет, равен ли размер Set 26. Обсудили асимптотику O(n) по времени и памяти, а также разницу между Map и Set.

Правильный ответ:

Вот несколько вариантов реализации проверки панограммы — от простого до оптимизированного.

1. Реализация с использованием Set

function isPangram(str) {
const letters = new Set();

for (const char of str.toLowerCase()) {
if (char >= 'a' && char <= 'z') {
letters.add(char);
}
}

return letters.size === 26;
}

// Примеры
console.log(isPangram('The quick brown fox jumps over the lazy dog')); // true
console.log(isPangram('Hello World')); // false

2. Компактная реализация через регулярное выражение

function isPangram(str) {
const uniqueLetters = new Set(str.toLowerCase().match(/[a-z]/g));
return uniqueLetters.size === 26;
}

3. Реализация с ранним выходом (оптимизация)

Если строка длинная, можно прекратить обработку сразу после нахождения всех 26 букв:

function isPangram(str) {
const letters = new Set();
const targetSize = 26;

for (const char of str.toLowerCase()) {
if (char >= 'a' && char <= 'z') {
letters.add(char);

// Ранний выход — все буквы найдены
if (letters.size === targetSize) {
return true;
}
}
}

return false;
}

4. Реализация через битовую маску (максимальная производительность)

Вместо Set можно использовать число как битовую маску, где каждый бит соответствует одной букве:

function isPangram(str) {
let mask = 0;
const allLetters = (1 << 26) - 1; // 0x3FFFFFF — 26 единичных битов

for (const char of str.toLowerCase()) {
if (char >= 'a' && char <= 'z') {
mask |= 1 << (char.charCodeAt(0) - 97);

if (mask === allLetters) {
return true;
}
}
}

return false;
}

5. Реализация через массив флагов

function isPangram(str) {
const seen = new Array(26).fill(false);
let count = 0;

for (const char of str.toLowerCase()) {
const index = char.charCodeAt(0) - 97;

if (index >= 0 && index < 26 && !seen[index]) {
seen[index] = true;
count++;

if (count === 26) {
return true;
}
}
}

return false;
}

Анализ сложности всех подходов:

ПодходВремяПамятьПримечание
SetO(n)O(1)**Множество не превышает 26 элементов
Битовая маскаO(n)O(1)Минимальное потребление памяти
Массив флаговO(n)O(1)Фиксированный массив из 26 элементов

Все подходы имеют линейную временную сложность O(n), где n — длина строки. Пространственная сложность формально O(1), так как размер хранилища ограничен 26 буквами и не зависит от входных данных.

Важные нюансы:

Регистрозависимость. Панограмма проверяется без учёта регистра, поэтому строку нужно привести к одному регистру перед проверкой.

Неалфавитные символы. Пробелы, цифры, знаки препинания и спецсимволы должны игнорироваться — проверяем только символы от 'a' до 'z'.

Способы проверки буквы:

// Способ 1: сравнение символов
char >= 'a' && char <= 'z'

// Способ 2: charCodeAt
const code = char.charCodeAt(0);
code >= 97 && code <= 122

// Способ 3: регулярное выражение
/[a-z]/.test(char)

Сравнение символов (char >= 'a') работает, потому что JavaScript сравнивает символы по их Unicode-кодам.

Примеры известных панограмм:

  • "The quick brown fox jumps over the lazy dog" — классическая панограмма
  • "Pack my box with five dozen liquor jugs"
  • "How vexingly quick daft zebras jump"

Вопрос 5. Реализовать функцию checkConditions, которая вызывает две функции checkResult одновременно и обрабатывает результаты по следующим правилам: если оба вернули true — вывести success, если хотя бы один вернул false — fail, если хотя бы один вернул ошибку — error, если ответ дольше 1 секунды — timeout.

Таймкод: 00:50:52

Ответ собеседника: Правильный. Кандидат реализовал функцию checkConditions, используя Promise.race для ожидания первого результата (либо ответа от функций, либо таймаута). Создал таймер на 1 секунду с помощью setTimeout. Если таймер срабатывает первым — выводится timeout. Если функции отвечают первыми — таймер очищается, и используется Promise.all для результатов обеих функций. Далее проверяются результаты: оба true — success, хотя бы один false — fail. Ошибки обрабатываются в catch блоке. В итоге решение было оптимизировано с использованием Promise.race с Promise.all и таймаутом.

Правильный ответ:

Вот несколько вариантов реализации checkConditions с обработкой таймаутов и ошибок.

1. Реализация с Promise.race (основной подход)

function checkConditions(checkResult1, checkResult2) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), 1000)
);

const combined = Promise.all([checkResult1(), checkResult2()]);

return Promise.race([combined, timeout])
.then(([result1, result2]) => {
if (result1 === false || result2 === false) {
return 'fail';
}
if (result1 === true && result2 === true) {
return 'success';
}
})
.catch(error => {
if (error.message === 'timeout') {
return 'timeout';
}
return 'error';
});
}

2. Реализация с явным управлением таймером

function checkConditions(checkResult1, checkResult2) {
return new Promise((resolve) => {
let timeoutId;
let resolved = false;

const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
if (!resolved) {
resolved = true;
resolve('timeout');
}
}, 1000);
});

const taskPromise = Promise.all([
Promise.resolve().then(() => checkResult1()),
Promise.resolve().then(() => checkResult2())
])
.then(([result1, result2]) => {
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);

if (result1 === false || result2 === false) {
resolve('fail');
} else if (result1 === true && result2 === true) {
resolve('success');
}
}
})
.catch(() => {
if (!resolved) {
resolved = true;
clearTimeout(timeoutId);
resolve('error');
}
});

Promise.race([taskPromise, timeoutPromise]);
});
}

3. Реализация с приоритетом ошибок (error > timeout > fail > success)

function checkConditions(checkResult1, checkResult2) {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject({ type: 'timeout' }), 1000)
);

const taskPromise = Promise.all([
Promise.resolve().then(() => checkResult1()),
Promise.resolve().then(() => checkResult2())
]);

return Promise.race([taskPromise, timeoutPromise])
.then(([result1, result2]) => {
if (result1 === false || result2 === false) {
return 'fail';
}
return 'success';
})
.catch(reason => {
if (reason.type === 'timeout') {
return 'timeout';
}
return 'error';
});
}

4. Реализация с возможностью отмены (AbortController)

function checkConditions(checkResult1, checkResult2) {
const controller = new AbortController();
const { signal } = controller;

const timeoutId = setTimeout(() => controller.abort(), 1000);

return Promise.all([
checkResult1(signal),
checkResult2(signal)
])
.then(([result1, result2]) => {
clearTimeout(timeoutId);

if (result1 === false || result2 === false) {
return 'fail';
}
return 'success';
})
.catch(error => {
clearTimeout(timeoutId);

if (error.name === 'AbortError') {
return 'timeout';
}
return 'error';
});
}

5. Реализация с детальной информацией о результате

function checkConditions(checkResult1, checkResult2) {
const timeout = (ms) => new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), ms)
);

const wrapWithTimeout = (fn) =>
Promise.race([fn(), timeout(1000)]);

return Promise.allSettled([
wrapWithTimeout(checkResult1),
wrapWithTimeout(checkResult2)
])
.then(results => {
const hasError = results.some(r => r.status === 'rejected');
const hasTimeout = results.some(
r => r.status === 'rejected' && r.reason.message === 'timeout'
);

if (hasTimeout) return 'timeout';
if (hasError) return 'error';

const [result1, result2] = results.map(r => r.value);

if (result1 === false || result2 === false) {
return 'fail';
}
return 'success';
});
}

Приоритет обработки результатов:

Важно определить приоритет при одновременном выполнении условий:

  1. timeout — если любая функция не ответила за 1 секунду
  2. error — если любая функция выбросила ошибку
  3. fail — если хотя бы один результат false
  4. success — только если оба результата true

Ключевые концепции:

Promise.race vs Promise.allSettled.

  • Promise.race — возвращает результат первого завершившегося Promise (успех или ошибка). Подходит для реализации таймаутов.
  • Promise.allSettled — ждёт завершения всех Promise и возвращает массив с результатами. Подходит когда нужны результаты всех операций.

Проблема "утечки" таймера. Если Promise резолвился раньше таймаута, таймер продолжает "висеть" в памяти. Важно очищать таймер через clearTimeout при завершении операции.

Обработка ошибок в Promise.all. Promise.all отклоняется при первой ошибке любого из Promise. Если нужно получить результаты всех Promise независимо от ошибок, используйте Promise.allSettled.

Пример использования:

// Тестовые функции
const fastTrue = () => Promise.resolve(true);
const fastFalse = () => Promise.resolve(false);
const slowTrue = () => new Promise(r => setTimeout(() => r(true), 2000));
const errorFn = () => Promise.reject(new Error('fail'));

// Тесты
await checkConditions(fastTrue, fastTrue); // 'success'
await checkConditions(fastTrue, fastFalse); // 'fail'
await checkConditions(fastTrue, errorFn); // 'error'
await checkConditions(fastTrue, slowTrue); // 'timeout'