diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
index d7c0d3cf096b0216c242c745ad2b815a2a5d1215..b4160c158c97a5af962474e61062e6ef4cbbd945 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java
@@ -10,6 +10,7 @@ import it.inaf.ia2.aa.data.User;
 import it.inaf.ia2.vospace.ui.VOSpaceUiApplication;
 import it.inaf.ia2.vospace.ui.data.Job;
 import it.inaf.ia2.vospace.ui.exception.BadRequestException;
+import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException;
 import it.inaf.ia2.vospace.ui.exception.VOSpaceException;
 import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
 import java.io.IOException;
@@ -256,7 +257,7 @@ public class VOSpaceClient {
                             return response;
                         }
                         logServerError(request, response);
-                        throw new VOSpaceException("Error calling " + request.uri().toString() + ". Server response code is " + response.statusCode());
+                        throw new VOSpaceStatusException("Error calling " + request.uri().toString() + ". Server response code is " + response.statusCode(), response.statusCode());
                     })
                     .thenApplyAsync(response -> {
                         HttpResponse<T> prev = response.previousResponse().orElse(null);
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java
index fb9a422d5222e4a9cd7ed306cdcbe7ad28ea1c13..e3fb16aa4754b8de23964b8b1ae8586f219e4e28 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java
@@ -6,8 +6,11 @@
 package it.inaf.ia2.vospace.ui.controller;
 
 import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
+import it.inaf.ia2.vospace.ui.data.PreUploadResult;
 import it.inaf.ia2.vospace.ui.data.UploadFilesData;
 import it.inaf.ia2.vospace.ui.exception.PermissionDeniedException;
+import it.inaf.ia2.vospace.ui.exception.VOSpaceException;
+import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException;
 import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
 import java.util.Arrays;
 import java.util.List;
@@ -18,6 +21,8 @@ import net.ivoa.xml.vospace.v2.DataNode;
 import net.ivoa.xml.vospace.v2.Property;
 import net.ivoa.xml.vospace.v2.Protocol;
 import net.ivoa.xml.vospace.v2.Transfer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.MediaType;
@@ -29,6 +34,8 @@ import org.springframework.web.bind.annotation.RestController;
 @RestController
 public class UploadController extends BaseController {
 
+    private static final Logger LOG = LoggerFactory.getLogger(UploadController.class);
+
     @Value("${vospace-authority}")
     private String authority;
 
@@ -36,17 +43,17 @@ public class UploadController extends BaseController {
     private VOSpaceClient client;
 
     @PostMapping(value = "/preupload", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
-    public ResponseEntity<List<String>> prepareForUpload(@RequestBody @Valid UploadFilesData data) {
+    public ResponseEntity<List<PreUploadResult>> prepareForUpload(@RequestBody @Valid UploadFilesData data) {
 
         if (getUser() == null) {
             throw new PermissionDeniedException("File upload not allowed to anonymous users");
         }
 
-        CompletableFuture<String>[] calls
+        CompletableFuture<PreUploadResult>[] calls
                 = data.getFiles().stream().map(fileName -> prepareForDownload(getParentPath(data), fileName))
                         .toArray(CompletableFuture[]::new);
 
-        List<String> uploadUrls = CompletableFuture.allOf(calls)
+        List<PreUploadResult> uploadUrls = CompletableFuture.allOf(calls)
                 .thenApplyAsync(ignore -> {
                     return Arrays.stream(calls).map(c -> c.join()).collect(Collectors.toList());
                 }).join();
@@ -62,7 +69,7 @@ public class UploadController extends BaseController {
         return parentPath;
     }
 
-    public CompletableFuture<String> prepareForDownload(String parentPath, String fileName) {
+    public CompletableFuture<PreUploadResult> prepareForDownload(String parentPath, String fileName) {
 
         return CompletableFuture.supplyAsync(() -> {
 
@@ -75,9 +82,31 @@ public class UploadController extends BaseController {
 
             String nodeUri = "vos://" + authority + urlEncodePath(path);
 
-            createDataNode(nodeUri, getUser().getName());
+            PreUploadResult result = new PreUploadResult();
+
+            try {
+                createDataNode(nodeUri, getUser().getName());
+            } catch (Throwable t) {
+                if (t instanceof VOSpaceStatusException && ((VOSpaceStatusException) t).getHttpStatus() == 409) {
+                    result.setError("Node already exists");
+                } else {
+                    LOG.error("Error while creating node metadata for " + nodeUri, t);
+                    result.setError("Unable to create node metadata");
+                }
+                return result;
+            }
+
+            try {
+                String uploadUrl = obtainUploadUrl(nodeUri);
+                result.setUrl(uploadUrl);
+            } catch (VOSpaceException ex) {
+                result.setError(ex.getMessage());
+            } catch (Throwable t) {
+                LOG.error("Error while obtaining upload URL for " + nodeUri, t);
+                result.setError("Unable to obtain upload URL");
+            }
 
-            return obtainUploadUrl(nodeUri);
+            return result;
         }, Runnable::run); // Passing current thread Executor to CompletableFuture to avoid "No thread-bound request found" exception
     }
 
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/PreUploadResult.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/PreUploadResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..47972bbba10e047f64d4cf85a88a29caeba68ee5
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/PreUploadResult.java
@@ -0,0 +1,35 @@
+/*
+ * This file is part of vospace-ui
+ * Copyright (C) 2021 Istituto Nazionale di Astrofisica
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.inaf.ia2.vospace.ui.data;
+
+/**
+ * Represents the result of the operations necessary before uploading a file
+ * (node metadata creation and generation of pushToVoSpace upload URL). If one
+ * of these operations fails the UI shows the error message for the specific
+ * file involved in the failure rather than aborting the whole upload (that can
+ * be composed by multiple files).
+ */
+public class PreUploadResult {
+
+    private String url;
+    private String error;
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getError() {
+        return error;
+    }
+
+    public void setError(String error) {
+        this.error = error;
+    }
+}
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/exception/VOSpaceStatusException.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/exception/VOSpaceStatusException.java
new file mode 100644
index 0000000000000000000000000000000000000000..70da49e24c39ad236bfc4bffca9b207e73f567c7
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/exception/VOSpaceStatusException.java
@@ -0,0 +1,20 @@
+/*
+ * This file is part of vospace-ui
+ * Copyright (C) 2021 Istituto Nazionale di Astrofisica
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.inaf.ia2.vospace.ui.exception;
+
+public class VOSpaceStatusException extends VOSpaceException {
+
+    private final int httpStatus;
+
+    public VOSpaceStatusException(String message, int httpStatus) {
+        super(message);
+        this.httpStatus = httpStatus;
+    }
+
+    public int getHttpStatus() {
+        return httpStatus;
+    }
+}
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/UploadControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/UploadControllerTest.java
index 18e8fdc415235326d5d637153daff2c03bb5be90..50e0459f2488dcceb27bff64e4ecb1ec3793a295 100644
--- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/UploadControllerTest.java
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/UploadControllerTest.java
@@ -9,11 +9,15 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import it.inaf.ia2.aa.data.User;
 import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
 import it.inaf.ia2.vospace.ui.data.UploadFilesData;
+import it.inaf.ia2.vospace.ui.exception.VOSpaceException;
+import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException;
 import java.util.List;
+import static org.hamcrest.core.IsNull.nullValue;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import static org.mockito.ArgumentMatchers.any;
 import org.mockito.Mock;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.when;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
@@ -63,7 +67,8 @@ public class UploadControllerTest {
                 .content(MAPPER.writeValueAsString(data)))
                 .andDo(print())
                 .andExpect(status().isOk())
-                .andExpect(jsonPath("$[0]").value("http://files/mynode/test.txt"));
+                .andExpect(jsonPath("$[0].url").value("http://files/mynode/test.txt"))
+                .andExpect(jsonPath("$[0].error").value(nullValue()));
     }
 
     @Test
@@ -81,7 +86,8 @@ public class UploadControllerTest {
                 .content(MAPPER.writeValueAsString(data)))
                 .andDo(print())
                 .andExpect(status().isOk())
-                .andExpect(jsonPath("$[0]").value("http://files/test.txt"));
+                .andExpect(jsonPath("$[0].url").value("http://files/test.txt"))
+                .andExpect(jsonPath("$[0].error").value(nullValue()));
     }
 
     @Test
@@ -96,4 +102,80 @@ public class UploadControllerTest {
                 .content(MAPPER.writeValueAsString(data)))
                 .andExpect(status().isForbidden());
     }
+
+    @Test
+    public void testDuplicatedNodeError() throws Exception {
+
+        UploadFilesData data = new UploadFilesData();
+        data.setParentPath("/mynode");
+        data.setFiles(List.of("test.txt"));
+
+        doThrow(new VOSpaceStatusException("Conflict", 409)).when(client).createNode(any());
+
+        mockMvc.perform(post("/preupload")
+                .sessionAttr("user_data", user)
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(MAPPER.writeValueAsString(data)))
+                .andDo(print())
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$[0].error").value("Node already exists"))
+                .andExpect(jsonPath("$[0].url").value(nullValue()));
+    }
+
+    @Test
+    public void testGenericMetadataCreationError() throws Exception {
+
+        UploadFilesData data = new UploadFilesData();
+        data.setParentPath("/mynode");
+        data.setFiles(List.of("test.txt"));
+
+        doThrow(new VOSpaceStatusException("Server Error", 500)).when(client).createNode(any());
+
+        mockMvc.perform(post("/preupload")
+                .sessionAttr("user_data", user)
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(MAPPER.writeValueAsString(data)))
+                .andDo(print())
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$[0].error").value("Unable to create node metadata"))
+                .andExpect(jsonPath("$[0].url").value(nullValue()));
+    }
+
+    @Test
+    public void testUploadUrlRecognizedError() throws Exception {
+
+        UploadFilesData data = new UploadFilesData();
+        data.setParentPath("/mynode");
+        data.setFiles(List.of("test.txt"));
+
+        doThrow(new VOSpaceException("Unable to connect")).when(client).getFileServiceEndpoint(any());
+
+        mockMvc.perform(post("/preupload")
+                .sessionAttr("user_data", user)
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(MAPPER.writeValueAsString(data)))
+                .andDo(print())
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$[0].error").value("Unable to connect"))
+                .andExpect(jsonPath("$[0].url").value(nullValue()));
+    }
+
+    @Test
+    public void testUploadUrlFatalError() throws Exception {
+
+        UploadFilesData data = new UploadFilesData();
+        data.setParentPath("/mynode");
+        data.setFiles(List.of("test.txt"));
+
+        doThrow(new NullPointerException()).when(client).getFileServiceEndpoint(any());
+
+        mockMvc.perform(post("/preupload")
+                .sessionAttr("user_data", user)
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(MAPPER.writeValueAsString(data)))
+                .andDo(print())
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$[0].error").value("Unable to obtain upload URL"))
+                .andExpect(jsonPath("$[0].url").value(nullValue()));
+    }
 }