<?xml version="1.0" encoding="utf-8" ?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:tt="http://teletype.in/" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/"><title>M41den ᴜɴᴛʏᴘᴇᴅ</title><author><name>M41den ᴜɴᴛʏᴘᴇᴅ</name></author><id>https://teletype.in/atom/m41den</id><link rel="self" type="application/atom+xml" href="https://teletype.in/atom/m41den?offset=0"></link><link rel="alternate" type="text/html" href="https://teletype.in/@m41den?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=m41den"></link><link rel="next" type="application/rss+xml" href="https://teletype.in/atom/m41den?offset=10"></link><link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></link><updated>2026-04-19T11:49:42.830Z</updated><entry><id>m41den:nextjs-server-actions-trickery</id><link rel="alternate" type="text/html" href="https://teletype.in/@m41den/nextjs-server-actions-trickery?utm_source=teletype&amp;utm_medium=feed_atom&amp;utm_campaign=m41den"></link><title>Next.js и исключения в Server Action</title><published>2025-04-25T16:39:20.559Z</published><updated>2025-04-25T16:51:41.270Z</updated><summary type="html">Если вы работали с Server Actions в Next.js, то знаете, насколько это удобно. Вызов серверных функций из клиента без RPC, возможность скрыть часть логики для тупой кнопки, валидация форм на сервере или просто способ скрыть API ключи и внутренний сервис. В общем очень круто... пока вы не начнете выкидывать ошибки, ожидая поймать их в клиенте.</summary><content type="html">
  &lt;p id=&quot;YKMe&quot;&gt;Если вы работали с Server Actions в Next.js, то знаете, насколько это удобно. Вызов серверных функций из клиента без RPC, возможность скрыть часть логики для тупой кнопки, валидация форм на сервере или просто способ скрыть API ключи и внутренний сервис. В общем очень круто... пока вы не начнете выкидывать ошибки, ожидая поймать их в клиенте.&lt;/p&gt;
  &lt;p id=&quot;eRjJ&quot;&gt;Посмотрим на этот кусок кода для входа через Supabase (не забываем &lt;code&gt;&amp;quot;use server&amp;quot;&lt;/code&gt;):&lt;/p&gt;
  &lt;figure id=&quot;rfr8&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/35/63/35639f8a-9f3c-43cf-9a5f-3514948d5b84.png&quot; width=&quot;1992&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;2y4X&quot;&gt;Выглядит нормально, наверное на стороне клиента мы ждем что-то типа&lt;/p&gt;
  &lt;figure id=&quot;CpNd&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img2.teletype.in/files/56/f2/56f2b6af-1aa3-4782-8045-cd9b70469b57.png&quot; width=&quot;852&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;UdC1&quot;&gt;И это абсолютно правильно, ну по крайней мере в Dev-окружении. Когда вы решите собрать продакшн-сборку, то столкнетесь с очень странным феноменом, от которого &lt;a href=&quot;https://github.com/vercel/next.js/issues/71101&quot; target=&quot;_blank&quot;&gt;полыхает у половины юзеров Next.js&lt;/a&gt;:&lt;/p&gt;
  &lt;figure id=&quot;EHnQ&quot; class=&quot;m_column&quot; data-caption-align=&quot;center&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/f1/df/f1dfa8fb-7adf-4895-aac9-199c139d97fa.png&quot; width=&quot;884&quot; /&gt;
    &lt;figcaption&gt;An error occured in the Server Components render. The specific message is ommited in production builds...&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;6CIw&quot;&gt;Опа... а где ошибка? Так вот же она, только у нее отобрали трейс и сообщение, оставили только хэш ошибки. Как говорят разрабочики из Vercel, &amp;quot;ето штобы вы дурачки трейсы с ключами апи и паролями клиентам не возвращали&amp;quot;. И даже если вы ловите ошибки в Sentry, а клиенту кидаете что-то типа&lt;/p&gt;
  &lt;pre id=&quot;8Uci&quot; data-lang=&quot;typescript&quot;&gt;try {
    ...
} catch (e) {
    sentry.pushEvent(e as Error)
    throw new Error(&amp;quot;Ты дурачок? Ты как нам бэк положил...&amp;quot;)
}&lt;/pre&gt;
  &lt;p id=&quot;tzFd&quot;&gt;то все-равно ваша ошибка будет съедена. Что делать? Передавать ошибки как пропы объектов. То есть теперь все Server Actions должны возвращать что-то вроде&lt;/p&gt;
  &lt;pre id=&quot;ZMd0&quot; data-lang=&quot;typescript&quot;&gt;type ActionReturnValue&amp;lt;T&amp;gt; = {
    data?: T,
    error?: Error
}&lt;/pre&gt;
  &lt;p id=&quot;szzz&quot;&gt;Но это ужас, а если мы где-то получим ошибку, где мы ее даже технически не ожидали? А что делать с этим дуализмом, когда наше значение превращается в &lt;/p&gt;
  &lt;pre id=&quot;MNFU&quot; data-lang=&quot;typescript&quot;&gt;const {data, error} = await myServerAction(...)
if (e &amp;amp;&amp;amp; e instanceof Error) {
    // Shitty error bruh
    ...
    return
}
const username = data?.username ?? &amp;quot;Unknown&amp;quot; // или data!.username&lt;/pre&gt;
  &lt;p id=&quot;bw95&quot;&gt;Это кринж полный. Однако это единственный выход. Правда можно все красиво оформить...&lt;/p&gt;
  &lt;p id=&quot;vHmi&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;rlsB&quot;&gt;Вот это копипастите к себе в проект, я сам написал, оно работает отлично и имхо лучше вариантов из гитхаба и статей на Medium:&lt;/p&gt;
  &lt;p id=&quot;p4Ts&quot;&gt;&lt;/p&gt;
  &lt;pre id=&quot;CJ6z&quot; data-lang=&quot;typescript&quot;&gt;export class ActionResponse {
    static success&amp;lt;T = unknown&amp;gt;(data: T): ActionData&amp;lt;T&amp;gt; {
        return {
            data
        }
    }

    static error&amp;lt;T = null&amp;gt;(error: Error): ActionData&amp;lt;T&amp;gt; {
        return {
            error: error.message
        }
    }

    static unwrap&amp;lt;T = null&amp;gt;(data: ActionData&amp;lt;T&amp;gt;): T {
        if (data.error)
            throw new Error(data.error)
        return data.data!
    }

    static wrap&amp;lt;P extends (...args: any) =&amp;gt; any&amp;gt;(func: P): ForwardedAsyncFunction&amp;lt;P&amp;gt; {
        return async (...args: ArgumentTypes&amp;lt;P&amp;gt;) =&amp;gt; {
            try {
                return ActionResponse.success(await func(...args))
            } catch (error) {
                return ActionResponse.error(error as Error)
            }
        }
    }
}

export type ActionData&amp;lt;T = unknown&amp;gt; = {
    data?: T,
    error?: string
}


type ForwardedAsyncFunction&amp;lt;F extends (...args: any) =&amp;gt; any&amp;gt; =
    (...args: ArgumentTypes&amp;lt;F&amp;gt;) =&amp;gt; Promise&amp;lt;ActionData&amp;lt;Awaited&amp;lt;ReturnType&amp;lt;F&amp;gt;&amp;gt;&amp;gt;&amp;gt;;


type ArgumentTypes&amp;lt;F extends Function&amp;gt; = 
    F extends (...args: infer A) =&amp;gt; unknown ? A : never;
&lt;/pre&gt;
  &lt;p id=&quot;P3Vc&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;J46j&quot;&gt;И так наш пример с логином превращается в &lt;/p&gt;
  &lt;figure id=&quot;ikY9&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img1.teletype.in/files/c0/56/c056ffc1-256c-4a67-983a-4836f217b0b4.png&quot; width=&quot;1816&quot; /&gt;
  &lt;/figure&gt;
  &lt;figure id=&quot;50ON&quot; class=&quot;m_column&quot;&gt;
    &lt;img src=&quot;https://img4.teletype.in/files/f8/00/f8005310-9cb6-43c3-b823-68b7724e619e.png&quot; width=&quot;958&quot; /&gt;
  &lt;/figure&gt;
  &lt;p id=&quot;gtI2&quot;&gt;Да, unwrap извлекает возвращаемое значение &lt;code&gt;{data}&lt;/code&gt; со всей типизацией.&lt;/p&gt;
  &lt;p id=&quot;dlJZ&quot;&gt;Завернуть функцию в Actions и развернуть на клиенте.&lt;/p&gt;
  &lt;p id=&quot;fm1F&quot;&gt;&lt;/p&gt;
  &lt;p id=&quot;7oei&quot;&gt;В принципе все, я просто опять наткнулся на unwrap в своем коде и подумал, что неплохо бы поделиться таким решением, так как я сутки потратил на мучение документации и проблем на гите Next.js - а тут спидран пройден.&lt;/p&gt;

</content></entry></feed>